| author | |
| committer | |
| log | 78d53062d1669c911cd938dd3a32faceafd6a57c |
| tree | 22db2df17bce3086ecb4b272d4b334771c3e629b |
| parent | 697d71d57d1f237bb104e1a933baac6b234477dd |
28 files changed, 46 insertions(+), 1010 deletions(-)
apps/server/integration/OrchestrationEngineHarness.integration.ts+1-1| ... | ... | @@ -188,7 +188,7 @@ export interface OrchestrationIntegrationHarness { |
| 188 | 188 | } |
| 189 | 189 | |
| 190 | 190 | interface MakeOrchestrationIntegrationHarnessOptions { |
| 191 | readonly provider?: "codex" | "cursor"; | |
| 191 | readonly provider?: "codex"; | |
| 192 | 192 | readonly realCodex?: boolean; |
| 193 | 193 | } |
| 194 | 194 |
apps/server/integration/TestProviderAdapter.integration.ts+5-5| ... | ... | @@ -35,7 +35,7 @@ export interface TestTurnResponse { |
| 35 | 35 | export type FixtureProviderRuntimeEvent = { |
| 36 | 36 | readonly type: string; |
| 37 | 37 | readonly eventId: EventId; |
| 38 | readonly provider: "codex" | "cursor"; | |
| 38 | readonly provider: "codex"; | |
| 39 | 39 | readonly createdAt: string; |
| 40 | 40 | readonly threadId: string; |
| 41 | 41 | readonly turnId?: string | undefined; |
| ... | ... | @@ -178,7 +178,7 @@ function normalizeFixtureEvent(rawEvent: Record<string, unknown>): ProviderRunti |
| 178 | 178 | |
| 179 | 179 | export interface TestProviderAdapterHarness { |
| 180 | 180 | readonly adapter: ProviderAdapterShape<ProviderAdapterError>; |
| 181 | readonly provider: "codex" | "cursor"; | |
| 181 | readonly provider: "codex"; | |
| 182 | 182 | readonly queueTurnResponse: ( |
| 183 | 183 | threadId: ThreadId, |
| 184 | 184 | response: TestTurnResponse, |
| ... | ... | @@ -198,7 +198,7 @@ export interface TestProviderAdapterHarness { |
| 198 | 198 | } |
| 199 | 199 | |
| 200 | 200 | interface MakeTestProviderAdapterHarnessOptions { |
| 201 | readonly provider?: "codex" | "cursor"; | |
| 201 | readonly provider?: "codex"; | |
| 202 | 202 | } |
| 203 | 203 | |
| 204 | 204 | function nowIso(): string { |
| ... | ... | @@ -206,7 +206,7 @@ function nowIso(): string { |
| 206 | 206 | } |
| 207 | 207 | |
| 208 | 208 | function sessionNotFound( |
| 209 | provider: "codex" | "cursor", | |
| 209 | provider: "codex", | |
| 210 | 210 | threadId: ThreadId, |
| 211 | 211 | ): ProviderAdapterSessionNotFoundError { |
| 212 | 212 | return new ProviderAdapterSessionNotFoundError({ |
| ... | ... | @@ -216,7 +216,7 @@ function sessionNotFound( |
| 216 | 216 | } |
| 217 | 217 | |
| 218 | 218 | function missingSessionEffect( |
| 219 | provider: "codex" | "cursor", | |
| 219 | provider: "codex", | |
| 220 | 220 | threadId: ThreadId, |
| 221 | 221 | ): Effect.Effect<never, ProviderAdapterError> { |
| 222 | 222 | return Effect.fail(sessionNotFound(provider, threadId)); |
apps/server/integration/orchestrationEngine.integration.test.ts+1-1| ... | ... | @@ -32,7 +32,7 @@ const PROJECT_ID = asProjectId("project-1"); |
| 32 | 32 | const THREAD_ID = ThreadId.makeUnsafe("thread-1"); |
| 33 | 33 | const FIXTURE_TURN_ID = "fixture-turn"; |
| 34 | 34 | const APPROVAL_REQUEST_ID = asApprovalRequestId("req-approval-1"); |
| 35 | type IntegrationProvider = "codex" | "cursor"; | |
| 35 | type IntegrationProvider = "codex"; | |
| 36 | 36 | |
| 37 | 37 | function nowIso() { |
| 38 | 38 | return new Date().toISOString(); |
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+1-1| ... | ... | @@ -43,7 +43,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); |
| 43 | 43 | type LegacyProviderRuntimeEvent = { |
| 44 | 44 | readonly type: string; |
| 45 | 45 | readonly eventId: EventId; |
| 46 | readonly provider: "codex" | "cursor"; | |
| 46 | readonly provider: "codex"; | |
| 47 | 47 | readonly createdAt: string; |
| 48 | 48 | readonly threadId: ThreadId; |
| 49 | 49 | readonly turnId?: string | undefined; |
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+4-147| ... | ... | @@ -93,8 +93,7 @@ describe("ProviderCommandReactor", () => { |
| 93 | 93 | typeof input === "object" && |
| 94 | 94 | input !== null && |
| 95 | 95 | "provider" in input && |
| 96 | (input.provider === "codex" || | |
| 97 | input.provider === "cursor") | |
| 96 | input.provider === "codex" | |
| 98 | 97 | ? input.provider |
| 99 | 98 | : "codex"; |
| 100 | 99 | const resumeCursor = |
| ... | ... | @@ -186,7 +185,7 @@ describe("ProviderCommandReactor", () => { |
| 186 | 185 | listSessions: () => Effect.succeed(runtimeSessions), |
| 187 | 186 | getCapabilities: (provider) => |
| 188 | 187 | Effect.succeed({ |
| 189 | sessionModelSwitch: provider === "cursor" ? "unsupported" : "in-session", | |
| 188 | sessionModelSwitch: provider === "codex" ? "in-session" : "in-session", | |
| 190 | 189 | }), |
| 191 | 190 | rollbackConversation: () => unsupported(), |
| 192 | 191 | stopAll: () => Effect.void, |
| ... | ... | @@ -384,43 +383,6 @@ describe("ProviderCommandReactor", () => { |
| 384 | 383 | }); |
| 385 | 384 | }); |
| 386 | 385 | |
| 387 | it("starts first turn with cursor provider when provider is specified", async () => { | |
| 388 | const harness = await createHarness(); | |
| 389 | const now = new Date().toISOString(); | |
| 390 | ||
| 391 | await Effect.runPromise( | |
| 392 | harness.engine.dispatch({ | |
| 393 | type: "thread.turn.start", | |
| 394 | commandId: CommandId.makeUnsafe("cmd-turn-start-provider-cursor"), | |
| 395 | threadId: ThreadId.makeUnsafe("thread-1"), | |
| 396 | message: { | |
| 397 | messageId: asMessageId("user-message-provider-cursor"), | |
| 398 | role: "user", | |
| 399 | text: "hello cursor", | |
| 400 | attachments: [], | |
| 401 | }, | |
| 402 | provider: "cursor", | |
| 403 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, | |
| 404 | runtimeMode: "approval-required", | |
| 405 | createdAt: now, | |
| 406 | }), | |
| 407 | ); | |
| 408 | ||
| 409 | await waitFor(() => harness.startSession.mock.calls.length === 1); | |
| 410 | await waitFor(() => harness.sendTurn.mock.calls.length === 1); | |
| 411 | expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({ | |
| 412 | provider: "cursor", | |
| 413 | cwd: "/tmp/provider-project", | |
| 414 | model: "gpt-5-codex", | |
| 415 | runtimeMode: "approval-required", | |
| 416 | }); | |
| 417 | ||
| 418 | const readModel = await Effect.runPromise(harness.engine.getReadModel()); | |
| 419 | const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); | |
| 420 | expect(thread?.session?.providerName).toBe("cursor"); | |
| 421 | expect(thread?.session?.threadId).toBe("thread-1"); | |
| 422 | }); | |
| 423 | ||
| 424 | 386 | it("reuses the same provider session when runtime mode is unchanged", async () => { |
| 425 | 387 | const harness = await createHarness(); |
| 426 | 388 | const now = new Date().toISOString(); |
| ... | ... | @@ -467,111 +429,6 @@ describe("ProviderCommandReactor", () => { |
| 467 | 429 | expect(harness.stopSession.mock.calls.length).toBe(0); |
| 468 | 430 | }); |
| 469 | 431 | |
| 470 | it("reuses the same cursor session when requested model is unchanged", async () => { | |
| 471 | const harness = await createHarness(); | |
| 472 | const now = new Date().toISOString(); | |
| 473 | ||
| 474 | await Effect.runPromise( | |
| 475 | harness.engine.dispatch({ | |
| 476 | type: "thread.turn.start", | |
| 477 | commandId: CommandId.makeUnsafe("cmd-turn-start-cursor-model-same-1"), | |
| 478 | threadId: ThreadId.makeUnsafe("thread-1"), | |
| 479 | message: { | |
| 480 | messageId: asMessageId("user-message-cursor-model-same-1"), | |
| 481 | role: "user", | |
| 482 | text: "first", | |
| 483 | attachments: [], | |
| 484 | }, | |
| 485 | provider: "cursor", | |
| 486 | model: "composer-1.5", | |
| 487 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, | |
| 488 | runtimeMode: "approval-required", | |
| 489 | createdAt: now, | |
| 490 | }), | |
| 491 | ); | |
| 492 | ||
| 493 | await waitFor(() => harness.startSession.mock.calls.length === 1); | |
| 494 | await waitFor(() => harness.sendTurn.mock.calls.length === 1); | |
| 495 | ||
| 496 | await Effect.runPromise( | |
| 497 | harness.engine.dispatch({ | |
| 498 | type: "thread.turn.start", | |
| 499 | commandId: CommandId.makeUnsafe("cmd-turn-start-cursor-model-same-2"), | |
| 500 | threadId: ThreadId.makeUnsafe("thread-1"), | |
| 501 | message: { | |
| 502 | messageId: asMessageId("user-message-cursor-model-same-2"), | |
| 503 | role: "user", | |
| 504 | text: "second", | |
| 505 | attachments: [], | |
| 506 | }, | |
| 507 | provider: "cursor", | |
| 508 | model: "composer-1.5", | |
| 509 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, | |
| 510 | runtimeMode: "approval-required", | |
| 511 | createdAt: now, | |
| 512 | }), | |
| 513 | ); | |
| 514 | ||
| 515 | await waitFor(() => harness.sendTurn.mock.calls.length === 2); | |
| 516 | expect(harness.startSession.mock.calls.length).toBe(1); | |
| 517 | expect(harness.stopSession.mock.calls.length).toBe(0); | |
| 518 | }); | |
| 519 | ||
| 520 | it("keeps cursor session/model when model change is unsupported", async () => { | |
| 521 | const harness = await createHarness(); | |
| 522 | const now = new Date().toISOString(); | |
| 523 | ||
| 524 | await Effect.runPromise( | |
| 525 | harness.engine.dispatch({ | |
| 526 | type: "thread.turn.start", | |
| 527 | commandId: CommandId.makeUnsafe("cmd-turn-start-cursor-model-change-1"), | |
| 528 | threadId: ThreadId.makeUnsafe("thread-1"), | |
| 529 | message: { | |
| 530 | messageId: asMessageId("user-message-cursor-model-change-1"), | |
| 531 | role: "user", | |
| 532 | text: "first", | |
| 533 | attachments: [], | |
| 534 | }, | |
| 535 | provider: "cursor", | |
| 536 | model: "gpt-5.3-codex", | |
| 537 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, | |
| 538 | runtimeMode: "approval-required", | |
| 539 | createdAt: now, | |
| 540 | }), | |
| 541 | ); | |
| 542 | ||
| 543 | await waitFor(() => harness.startSession.mock.calls.length === 1); | |
| 544 | await waitFor(() => harness.sendTurn.mock.calls.length === 1); | |
| 545 | ||
| 546 | await Effect.runPromise( | |
| 547 | harness.engine.dispatch({ | |
| 548 | type: "thread.turn.start", | |
| 549 | commandId: CommandId.makeUnsafe("cmd-turn-start-cursor-model-change-2"), | |
| 550 | threadId: ThreadId.makeUnsafe("thread-1"), | |
| 551 | message: { | |
| 552 | messageId: asMessageId("user-message-cursor-model-change-2"), | |
| 553 | role: "user", | |
| 554 | text: "second", | |
| 555 | attachments: [], | |
| 556 | }, | |
| 557 | provider: "cursor", | |
| 558 | model: "composer-1.5", | |
| 559 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, | |
| 560 | runtimeMode: "approval-required", | |
| 561 | createdAt: now, | |
| 562 | }), | |
| 563 | ); | |
| 564 | ||
| 565 | await waitFor(() => harness.sendTurn.mock.calls.length === 2); | |
| 566 | ||
| 567 | expect(harness.stopSession.mock.calls.length).toBe(0); | |
| 568 | expect(harness.startSession.mock.calls.length).toBe(1); | |
| 569 | expect(harness.sendTurn.mock.calls[1]?.[0]).toMatchObject({ | |
| 570 | threadId: ThreadId.makeUnsafe("thread-1"), | |
| 571 | model: "gpt-5.3-codex", | |
| 572 | }); | |
| 573 | }); | |
| 574 | ||
| 575 | 432 | it("restarts the provider session when runtime mode is updated on the thread", async () => { |
| 576 | 433 | const harness = await createHarness(); |
| 577 | 434 | const now = new Date().toISOString(); |
| ... | ... | @@ -852,7 +709,7 @@ describe("ProviderCommandReactor", () => { |
| 852 | 709 | harness.respondToRequest.mockImplementation(() => |
| 853 | 710 | Effect.fail( |
| 854 | 711 | new ProviderAdapterRequestError({ |
| 855 | provider: "cursor", | |
| 712 | provider: "codex", | |
| 856 | 713 | method: "session/request_permission", |
| 857 | 714 | detail: "Unknown pending permission request: approval-request-1", |
| 858 | 715 | }), |
| ... | ... | @@ -867,7 +724,7 @@ describe("ProviderCommandReactor", () => { |
| 867 | 724 | session: { |
| 868 | 725 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 869 | 726 | status: "running", |
| 870 | providerName: "cursor", | |
| 727 | providerName: "codex", | |
| 871 | 728 | runtimeMode: "approval-required", |
| 872 | 729 | activeTurnId: null, |
| 873 | 730 | lastError: null, |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+1-4| ... | ... | @@ -211,10 +211,7 @@ const make = Effect.gen(function* () { |
| 211 | 211 | |
| 212 | 212 | const desiredRuntimeMode = thread.runtimeMode; |
| 213 | 213 | const currentProvider: ProviderKind | undefined = |
| 214 | thread.session?.providerName === "codex" || | |
| 215 | thread.session?.providerName === "cursor" | |
| 216 | ? thread.session.providerName | |
| 217 | : undefined; | |
| 214 | thread.session?.providerName === "codex" ? thread.session.providerName : undefined; | |
| 218 | 215 | const preferredProvider: ProviderKind | undefined = options?.provider ?? currentProvider; |
| 219 | 216 | const desiredModel = options?.model ?? thread.model; |
| 220 | 217 | const effectiveCwd = resolveThreadWorkspaceCwd({ |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+1-1| ... | ... | @@ -41,7 +41,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); |
| 41 | 41 | type LegacyProviderRuntimeEvent = { |
| 42 | 42 | readonly type: string; |
| 43 | 43 | readonly eventId: EventId; |
| 44 | readonly provider: "codex" | "cursor"; | |
| 44 | readonly provider: "codex"; | |
| 45 | 45 | readonly createdAt: string; |
| 46 | 46 | readonly threadId: ThreadId; |
| 47 | 47 | readonly turnId?: string | undefined; |
apps/server/src/provider/Layers/CodexAdapter.test.ts-24| ... | ... | @@ -157,30 +157,6 @@ const validationLayer = it.layer( |
| 157 | 157 | ); |
| 158 | 158 | |
| 159 | 159 | validationLayer("CodexAdapterLive validation", (it) => { |
| 160 | it.effect("returns validation error for non-codex provider on startSession", () => | |
| 161 | Effect.gen(function* () { | |
| 162 | const adapter = yield* CodexAdapter; | |
| 163 | const result = yield* adapter | |
| 164 | .startSession({ | |
| 165 | provider: "cursor", | |
| 166 | threadId: asThreadId("thread-1"), | |
| 167 | runtimeMode: "full-access", | |
| 168 | }) | |
| 169 | .pipe(Effect.result); | |
| 170 | ||
| 171 | assert.equal(result._tag, "Failure"); | |
| 172 | assert.deepStrictEqual( | |
| 173 | result.failure, | |
| 174 | new ProviderAdapterValidationError({ | |
| 175 | provider: "codex", | |
| 176 | operation: "startSession", | |
| 177 | issue: "Expected provider 'codex' but received 'cursor'.", | |
| 178 | }), | |
| 179 | ); | |
| 180 | assert.equal(validationManager.startSessionImpl.mock.calls.length, 0); | |
| 181 | }), | |
| 182 | ); | |
| 183 | ||
| 184 | 160 | it.effect("maps codex model options before starting a session", () => |
| 185 | 161 | Effect.gen(function* () { |
| 186 | 162 | validationManager.startSessionImpl.mockClear(); |
apps/server/src/provider/Layers/ProviderService.test.ts+1-1| ... | ... | @@ -51,7 +51,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); |
| 51 | 51 | type LegacyProviderRuntimeEvent = { |
| 52 | 52 | readonly type: string; |
| 53 | 53 | readonly eventId: EventId; |
| 54 | readonly provider: "codex" | "cursor"; | |
| 54 | readonly provider: "codex"; | |
| 55 | 55 | readonly createdAt: string; |
| 56 | 56 | readonly threadId: ThreadId; |
| 57 | 57 | readonly turnId?: string | undefined; |
apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts-22| ... | ... | @@ -174,26 +174,4 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 174 | 174 | |
| 175 | 175 | fs.rmSync(tempDir, { recursive: true, force: true }); |
| 176 | 176 | })); |
| 177 | ||
| 178 | it("accepts cursor provider bindings", () => | |
| 179 | Effect.gen(function* () { | |
| 180 | const directory = yield* ProviderSessionDirectory; | |
| 181 | const threadId = ThreadId.makeUnsafe("thread-cursor"); | |
| 182 | ||
| 183 | yield* directory.upsert({ | |
| 184 | provider: "cursor", | |
| 185 | threadId, | |
| 186 | }); | |
| 187 | ||
| 188 | const provider = yield* directory.getProvider(threadId); | |
| 189 | assert.equal(provider, "cursor"); | |
| 190 | const resolvedBinding = yield* directory.getBinding(threadId); | |
| 191 | assertSome(resolvedBinding, { | |
| 192 | threadId, | |
| 193 | provider: "cursor", | |
| 194 | }); | |
| 195 | if (Option.isSome(resolvedBinding)) { | |
| 196 | assert.equal(resolvedBinding.value.threadId, threadId); | |
| 197 | } | |
| 198 | })); | |
| 199 | 177 | }); |
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+1-1| ... | ... | @@ -25,7 +25,7 @@ function decodeProviderKind( |
| 25 | 25 | providerName: string, |
| 26 | 26 | operation: string, |
| 27 | 27 | ): Effect.Effect<ProviderKind, ProviderSessionDirectoryPersistenceError> { |
| 28 | if (providerName === "codex" || providerName === "cursor") { | |
| 28 | if (providerName === "codex") { | |
| 29 | 29 | return Effect.succeed(providerName); |
| 30 | 30 | } |
| 31 | 31 | return Effect.fail( |
apps/web/src/appSettings.test.ts-21| ... | ... | @@ -15,13 +15,6 @@ describe("normalizeCustomModelSlugs", () => { |
| 15 | 15 | ]), |
| 16 | 16 | ).toEqual(["custom/internal-model"]); |
| 17 | 17 | }); |
| 18 | ||
| 19 | it("normalizes provider-specific aliases for cursor", () => { | |
| 20 | expect(normalizeCustomModelSlugs(["composer"], "cursor")).toEqual([]); | |
| 21 | expect(normalizeCustomModelSlugs(["cursor/custom-model"], "cursor")).toEqual([ | |
| 22 | "cursor/custom-model", | |
| 23 | ]); | |
| 24 | }); | |
| 25 | 18 | }); |
| 26 | 19 | |
| 27 | 20 | describe("getAppModelOptions", () => { |
| ... | ... | @@ -47,14 +40,6 @@ describe("getAppModelOptions", () => { |
| 47 | 40 | isCustom: true, |
| 48 | 41 | }); |
| 49 | 42 | }); |
| 50 | ||
| 51 | it("keeps a saved custom provider model available as an exact slug option", () => { | |
| 52 | const options = getAppModelOptions("cursor", ["cursor/custom-model"], "cursor/custom-model"); | |
| 53 | ||
| 54 | expect(options.some((option) => option.slug === "cursor/custom-model" && option.isCustom)).toBe( | |
| 55 | true, | |
| 56 | ); | |
| 57 | }); | |
| 58 | 43 | }); |
| 59 | 44 | |
| 60 | 45 | describe("getSlashModelOptions", () => { |
| ... | ... | @@ -79,10 +64,4 @@ describe("getSlashModelOptions", () => { |
| 79 | 64 | |
| 80 | 65 | expect(options.map((option) => option.slug)).toEqual(["openai/gpt-oss-120b"]); |
| 81 | 66 | }); |
| 82 | ||
| 83 | it("includes provider-specific custom slugs in non-codex model lists", () => { | |
| 84 | const cursorOptions = getAppModelOptions("cursor", ["cursor/custom-model"]); | |
| 85 | ||
| 86 | expect(cursorOptions.some((option) => option.slug === "cursor/custom-model")).toBe(true); | |
| 87 | }); | |
| 88 | 67 | }); |
apps/web/src/appSettings.ts-5| ... | ... | @@ -8,7 +8,6 @@ const MAX_CUSTOM_MODEL_COUNT = 32; |
| 8 | 8 | export const MAX_CUSTOM_MODEL_LENGTH = 256; |
| 9 | 9 | const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record<ProviderKind, ReadonlySet<string>> = { |
| 10 | 10 | codex: new Set(getModelOptions("codex").map((option) => option.slug)), |
| 11 | cursor: new Set(getModelOptions("cursor").map((option) => option.slug)), | |
| 12 | 11 | }; |
| 13 | 12 | |
| 14 | 13 | const AppSettingsSchema = Schema.Struct({ |
| ... | ... | @@ -25,9 +24,6 @@ const AppSettingsSchema = Schema.Struct({ |
| 25 | 24 | customCodexModels: Schema.Array(Schema.String).pipe( |
| 26 | 25 | Schema.withConstructorDefault(() => Option.some([])), |
| 27 | 26 | ), |
| 28 | customCursorModels: Schema.Array(Schema.String).pipe( | |
| 29 | Schema.withConstructorDefault(() => Option.some([])), | |
| 30 | ), | |
| 31 | 27 | }); |
| 32 | 28 | export type AppSettings = typeof AppSettingsSchema.Type; |
| 33 | 29 | export interface AppModelOption { |
| ... | ... | @@ -75,7 +71,6 @@ function normalizeAppSettings(settings: AppSettings): AppSettings { |
| 75 | 71 | return { |
| 76 | 72 | ...settings, |
| 77 | 73 | customCodexModels: normalizeCustomModelSlugs(settings.customCodexModels, "codex"), |
| 78 | customCursorModels: normalizeCustomModelSlugs(settings.customCursorModels, "cursor"), | |
| 79 | 74 | }; |
| 80 | 75 | } |
| 81 | 76 |
apps/web/src/components/ChatView.tsx+12-287| ... | ... | @@ -1,12 +1,10 @@ |
| 1 | 1 | import { |
| 2 | 2 | type ApprovalRequestId, |
| 3 | CURSOR_REASONING_OPTIONS, | |
| 4 | 3 | DEFAULT_MODEL, |
| 5 | 4 | EDITORS, |
| 6 | 5 | type EditorId, |
| 7 | 6 | type KeybindingCommand, |
| 8 | 7 | type CodexReasoningEffort, |
| 9 | type CursorReasoningOption, | |
| 10 | 8 | type MessageId, |
| 11 | 9 | type ProjectId, |
| 12 | 10 | type ProjectEntry, |
| ... | ... | @@ -27,12 +25,8 @@ import { |
| 27 | 25 | import { |
| 28 | 26 | getDefaultModel, |
| 29 | 27 | getDefaultReasoningEffort, |
| 30 | getCursorModelCapabilities, | |
| 31 | getCursorModelFamilyOptions, | |
| 32 | 28 | getReasoningEffortOptions, |
| 33 | 29 | normalizeModelSlug, |
| 34 | parseCursorModelSelection, | |
| 35 | resolveCursorModelFromSelection, | |
| 36 | 30 | resolveModelSlugForProvider, |
| 37 | 31 | } from "@t3tools/shared/model"; |
| 38 | 32 | import { |
| ... | ... | @@ -761,10 +755,6 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 761 | 755 | ? (sessionProvider ?? selectedProviderByThreadId ?? null) |
| 762 | 756 | : null; |
| 763 | 757 | const selectedProvider: ProviderKind = lockedProvider ?? selectedProviderByThreadId ?? "codex"; |
| 764 | const cursorModelSelectionLockedReason = | |
| 765 | hasThreadStarted && selectedProvider === "cursor" | |
| 766 | ? "Cursor currently does not support changing models after the first message in a thread." | |
| 767 | : null; | |
| 768 | 758 | const baseThreadModel = resolveModelSlugForProvider( |
| 769 | 759 | selectedProvider, |
| 770 | 760 | activeThread?.model ?? |
| ... | ... | @@ -811,40 +801,16 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 811 | 801 | }; |
| 812 | 802 | return Object.keys(codexOptions).length > 0 ? { codex: codexOptions } : undefined; |
| 813 | 803 | }, [selectedCodexFastModeEnabled, selectedEffort, selectedProvider, supportsReasoningEffort]); |
| 814 | const selectedCursorModel = useMemo( | |
| 815 | () => (selectedProvider === "cursor" ? parseCursorModelSelection(selectedModel) : null), | |
| 816 | [selectedModel, selectedProvider], | |
| 817 | ); | |
| 818 | const selectedCursorModelCapabilities = useMemo( | |
| 819 | () => (selectedCursorModel ? getCursorModelCapabilities(selectedCursorModel.family) : null), | |
| 820 | [selectedCursorModel], | |
| 821 | ); | |
| 822 | const hasSelectedCursorTraits = Boolean( | |
| 823 | selectedCursorModelCapabilities && | |
| 824 | (selectedCursorModelCapabilities.supportsReasoning || | |
| 825 | selectedCursorModelCapabilities.supportsFast || | |
| 826 | selectedCursorModelCapabilities.supportsThinking), | |
| 827 | ); | |
| 828 | const selectedModelForPicker = | |
| 829 | selectedProvider === "cursor" && selectedCursorModel | |
| 830 | ? selectedCursorModel.family | |
| 831 | : selectedModel; | |
| 804 | const selectedModelForPicker = selectedModel; | |
| 832 | 805 | const modelOptionsByProvider = useMemo( |
| 833 | 806 | () => getCustomModelOptionsByProvider(settings), |
| 834 | 807 | [settings], |
| 835 | 808 | ); |
| 836 | 809 | const selectedModelForPickerWithCustomFallback = useMemo(() => { |
| 837 | if (selectedProvider !== "cursor") { | |
| 838 | const currentOptions = modelOptionsByProvider[selectedProvider]; | |
| 839 | return currentOptions.some((option) => option.slug === selectedModelForPicker) | |
| 840 | ? selectedModelForPicker | |
| 841 | : (normalizeModelSlug(selectedModelForPicker, selectedProvider) ?? selectedModelForPicker); | |
| 842 | } | |
| 843 | ||
| 844 | const currentOptions = modelOptionsByProvider.cursor; | |
| 810 | const currentOptions = modelOptionsByProvider[selectedProvider]; | |
| 845 | 811 | return currentOptions.some((option) => option.slug === selectedModelForPicker) |
| 846 | 812 | ? selectedModelForPicker |
| 847 | : selectedModelForPicker; | |
| 813 | : (normalizeModelSlug(selectedModelForPicker, selectedProvider) ?? selectedModelForPicker); | |
| 848 | 814 | }, [modelOptionsByProvider, selectedModelForPicker, selectedProvider]); |
| 849 | 815 | const searchableModelOptions = useMemo( |
| 850 | 816 | () => |
| ... | ... | @@ -3009,73 +2975,23 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3009 | 2975 | const onProviderModelSelect = useCallback( |
| 3010 | 2976 | (provider: ProviderKind, model: ModelSlug) => { |
| 3011 | 2977 | if (!activeThread) return; |
| 3012 | if (cursorModelSelectionLockedReason !== null && provider === "cursor") { | |
| 3013 | scheduleComposerFocus(); | |
| 3014 | return; | |
| 3015 | } | |
| 3016 | 2978 | if (lockedProvider !== null && provider !== lockedProvider) { |
| 3017 | 2979 | scheduleComposerFocus(); |
| 3018 | 2980 | return; |
| 3019 | 2981 | } |
| 3020 | const resolvedModel = | |
| 3021 | provider === "cursor" | |
| 3022 | ? resolveCursorModelFromSelection(parseCursorModelSelection(model)) | |
| 3023 | : resolveModelSlugForProvider(provider, model); | |
| 2982 | const resolvedModel = resolveModelSlugForProvider(provider, model); | |
| 3024 | 2983 | setComposerDraftProvider(activeThread.id, provider); |
| 3025 | 2984 | setComposerDraftModel(activeThread.id, resolvedModel); |
| 3026 | 2985 | scheduleComposerFocus(); |
| 3027 | 2986 | }, |
| 3028 | 2987 | [ |
| 3029 | 2988 | activeThread, |
| 3030 | cursorModelSelectionLockedReason, | |
| 3031 | 2989 | lockedProvider, |
| 3032 | 2990 | scheduleComposerFocus, |
| 3033 | 2991 | setComposerDraftModel, |
| 3034 | 2992 | setComposerDraftProvider, |
| 3035 | 2993 | ], |
| 3036 | 2994 | ); |
| 3037 | const onCursorReasoningSelect = useCallback( | |
| 3038 | (reasoning: CursorReasoningOption) => { | |
| 3039 | if (selectedProvider !== "cursor") return; | |
| 3040 | const cursorSelection = parseCursorModelSelection(selectedModel); | |
| 3041 | const nextModel = resolveCursorModelFromSelection({ | |
| 3042 | family: cursorSelection.family, | |
| 3043 | reasoning, | |
| 3044 | fast: cursorSelection.fast, | |
| 3045 | thinking: cursorSelection.thinking, | |
| 3046 | }); | |
| 3047 | onProviderModelSelect("cursor", nextModel); | |
| 3048 | }, | |
| 3049 | [onProviderModelSelect, selectedModel, selectedProvider], | |
| 3050 | ); | |
| 3051 | const onCursorFastModeChange = useCallback( | |
| 3052 | (enabled: boolean) => { | |
| 3053 | if (selectedProvider !== "cursor") return; | |
| 3054 | const cursorSelection = parseCursorModelSelection(selectedModel); | |
| 3055 | const nextModel = resolveCursorModelFromSelection({ | |
| 3056 | family: cursorSelection.family, | |
| 3057 | reasoning: cursorSelection.reasoning, | |
| 3058 | fast: enabled, | |
| 3059 | thinking: cursorSelection.thinking, | |
| 3060 | }); | |
| 3061 | onProviderModelSelect("cursor", nextModel); | |
| 3062 | }, | |
| 3063 | [onProviderModelSelect, selectedModel, selectedProvider], | |
| 3064 | ); | |
| 3065 | const onCursorThinkingModeChange = useCallback( | |
| 3066 | (enabled: boolean) => { | |
| 3067 | if (selectedProvider !== "cursor") return; | |
| 3068 | const cursorSelection = parseCursorModelSelection(selectedModel); | |
| 3069 | const nextModel = resolveCursorModelFromSelection({ | |
| 3070 | family: cursorSelection.family, | |
| 3071 | reasoning: cursorSelection.reasoning, | |
| 3072 | fast: cursorSelection.fast, | |
| 3073 | thinking: enabled, | |
| 3074 | }); | |
| 3075 | onProviderModelSelect("cursor", nextModel); | |
| 3076 | }, | |
| 3077 | [onProviderModelSelect, selectedModel, selectedProvider], | |
| 3078 | ); | |
| 3079 | 2995 | const onEffortSelect = useCallback( |
| 3080 | 2996 | (effort: CodexReasoningEffort) => { |
| 3081 | 2997 | setComposerDraftEffort(threadId, effort); |
| ... | ... | @@ -3594,82 +3510,15 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3594 | 3510 | <div className="flex flex-wrap items-center justify-between gap-2 px-2.5 pb-2.5 sm:flex-nowrap sm:gap-0 sm:px-3 sm:pb-3"> |
| 3595 | 3511 | <div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden sm:min-w-max sm:overflow-visible"> |
| 3596 | 3512 | {/* Provider/model picker */} |
| 3597 | {cursorModelSelectionLockedReason ? ( | |
| 3598 | <Tooltip> | |
| 3599 | <TooltipTrigger | |
| 3600 | render={ | |
| 3601 | <span className="inline-flex"> | |
| 3602 | <ProviderModelPicker | |
| 3603 | provider={selectedProvider} | |
| 3604 | model={selectedModelForPickerWithCustomFallback} | |
| 3605 | lockedProvider={lockedProvider} | |
| 3606 | modelOptionsByProvider={modelOptionsByProvider} | |
| 3607 | disabled | |
| 3608 | onProviderModelChange={onProviderModelSelect} | |
| 3609 | /> | |
| 3610 | </span> | |
| 3611 | } | |
| 3612 | /> | |
| 3613 | <TooltipPopup side="top" className="max-w-72 whitespace-normal leading-tight"> | |
| 3614 | {cursorModelSelectionLockedReason} | |
| 3615 | </TooltipPopup> | |
| 3616 | </Tooltip> | |
| 3617 | ) : ( | |
| 3618 | <ProviderModelPicker | |
| 3619 | provider={selectedProvider} | |
| 3620 | model={selectedModelForPickerWithCustomFallback} | |
| 3621 | lockedProvider={lockedProvider} | |
| 3622 | modelOptionsByProvider={modelOptionsByProvider} | |
| 3623 | onProviderModelChange={onProviderModelSelect} | |
| 3624 | /> | |
| 3625 | )} | |
| 3626 | ||
| 3627 | {selectedProvider === "cursor" ? ( | |
| 3628 | <> | |
| 3629 | {hasSelectedCursorTraits && ( | |
| 3630 | <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" /> | |
| 3631 | )} | |
| 3513 | <ProviderModelPicker | |
| 3514 | provider={selectedProvider} | |
| 3515 | model={selectedModelForPickerWithCustomFallback} | |
| 3516 | lockedProvider={lockedProvider} | |
| 3517 | modelOptionsByProvider={modelOptionsByProvider} | |
| 3518 | onProviderModelChange={onProviderModelSelect} | |
| 3519 | /> | |
| 3632 | 3520 | |
| 3633 | {selectedCursorModel && | |
| 3634 | selectedCursorModelCapabilities && | |
| 3635 | hasSelectedCursorTraits && ( | |
| 3636 | <> | |
| 3637 | {cursorModelSelectionLockedReason ? ( | |
| 3638 | <Tooltip> | |
| 3639 | <TooltipTrigger | |
| 3640 | render={ | |
| 3641 | <span className="inline-flex"> | |
| 3642 | <CursorTraitsPicker | |
| 3643 | selection={selectedCursorModel} | |
| 3644 | capabilities={selectedCursorModelCapabilities} | |
| 3645 | disabled | |
| 3646 | onReasoningChange={onCursorReasoningSelect} | |
| 3647 | onFastModeChange={onCursorFastModeChange} | |
| 3648 | onThinkingModeChange={onCursorThinkingModeChange} | |
| 3649 | /> | |
| 3650 | </span> | |
| 3651 | } | |
| 3652 | /> | |
| 3653 | <TooltipPopup | |
| 3654 | side="top" | |
| 3655 | className="max-w-72 whitespace-normal leading-tight" | |
| 3656 | > | |
| 3657 | {cursorModelSelectionLockedReason} | |
| 3658 | </TooltipPopup> | |
| 3659 | </Tooltip> | |
| 3660 | ) : ( | |
| 3661 | <CursorTraitsPicker | |
| 3662 | selection={selectedCursorModel} | |
| 3663 | capabilities={selectedCursorModelCapabilities} | |
| 3664 | onReasoningChange={onCursorReasoningSelect} | |
| 3665 | onFastModeChange={onCursorFastModeChange} | |
| 3666 | onThinkingModeChange={onCursorThinkingModeChange} | |
| 3667 | /> | |
| 3668 | )} | |
| 3669 | </> | |
| 3670 | )} | |
| 3671 | </> | |
| 3672 | ) : selectedProvider === "codex" && selectedEffort != null ? ( | |
| 3521 | {selectedProvider === "codex" && selectedEffort != null ? ( | |
| 3673 | 3522 | <> |
| 3674 | 3523 | <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" /> |
| 3675 | 3524 | <CodexTraitsPicker |
| ... | ... | @@ -5297,18 +5146,9 @@ const COMING_SOON_PROVIDER_OPTIONS = [ |
| 5297 | 5146 | |
| 5298 | 5147 | function getCustomModelOptionsByProvider(settings: { |
| 5299 | 5148 | customCodexModels: readonly string[]; |
| 5300 | customCursorModels: readonly string[]; | |
| 5301 | 5149 | }): Record<ProviderKind, ReadonlyArray<{ slug: string; name: string }>> { |
| 5302 | const cursorFamilyOptions = getCursorModelFamilyOptions(); | |
| 5303 | 5150 | return { |
| 5304 | 5151 | codex: getAppModelOptions("codex", settings.customCodexModels), |
| 5305 | cursor: [ | |
| 5306 | ...cursorFamilyOptions, | |
| 5307 | ...getAppModelOptions("cursor", settings.customCursorModels).filter( | |
| 5308 | (option) => | |
| 5309 | option.isCustom && !cursorFamilyOptions.some((family) => family.slug === option.slug), | |
| 5310 | ), | |
| 5311 | ], | |
| 5312 | 5152 | }; |
| 5313 | 5153 | } |
| 5314 | 5154 | |
| ... | ... | @@ -5348,10 +5188,6 @@ function resolveModelForProviderPicker( |
| 5348 | 5188 | return resolved.slug; |
| 5349 | 5189 | } |
| 5350 | 5190 | |
| 5351 | if (provider === "cursor") { | |
| 5352 | return parseCursorModelSelection(normalized).family; | |
| 5353 | } | |
| 5354 | ||
| 5355 | 5191 | return null; |
| 5356 | 5192 | } |
| 5357 | 5193 | |
| ... | ... | @@ -5562,117 +5398,6 @@ const CodexTraitsPicker = memo(function CodexTraitsPicker(props: { |
| 5562 | 5398 | ); |
| 5563 | 5399 | }); |
| 5564 | 5400 | |
| 5565 | const CursorTraitsPicker = memo(function CursorTraitsPicker(props: { | |
| 5566 | selection: ReturnType<typeof parseCursorModelSelection>; | |
| 5567 | capabilities: ReturnType<typeof getCursorModelCapabilities>; | |
| 5568 | disabled?: boolean; | |
| 5569 | onReasoningChange: (reasoning: CursorReasoningOption) => void; | |
| 5570 | onFastModeChange: (enabled: boolean) => void; | |
| 5571 | onThinkingModeChange: (enabled: boolean) => void; | |
| 5572 | }) { | |
| 5573 | const [isMenuOpen, setIsMenuOpen] = useState(false); | |
| 5574 | const reasoningLabelByOption: Record<CursorReasoningOption, string> = { | |
| 5575 | low: "Low", | |
| 5576 | normal: "Normal", | |
| 5577 | high: "High", | |
| 5578 | xhigh: "Extra High", | |
| 5579 | }; | |
| 5580 | const traitSummary = [ | |
| 5581 | ...(props.capabilities.supportsReasoning | |
| 5582 | ? [reasoningLabelByOption[props.selection.reasoning]] | |
| 5583 | : []), | |
| 5584 | ...(props.capabilities.supportsFast && props.selection.fast ? ["Fast"] : []), | |
| 5585 | ...(props.capabilities.supportsThinking && props.selection.thinking ? ["Thinking"] : []), | |
| 5586 | ]; | |
| 5587 | const triggerLabel = traitSummary.length > 0 ? traitSummary.join(" · ") : "Traits"; | |
| 5588 | ||
| 5589 | return ( | |
| 5590 | <Menu | |
| 5591 | open={isMenuOpen} | |
| 5592 | onOpenChange={(open) => { | |
| 5593 | if (props.disabled) { | |
| 5594 | setIsMenuOpen(false); | |
| 5595 | return; | |
| 5596 | } | |
| 5597 | setIsMenuOpen(open); | |
| 5598 | }} | |
| 5599 | > | |
| 5600 | <MenuTrigger | |
| 5601 | render={ | |
| 5602 | <Button | |
| 5603 | size="sm" | |
| 5604 | variant="ghost" | |
| 5605 | className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3" | |
| 5606 | disabled={props.disabled} | |
| 5607 | /> | |
| 5608 | } | |
| 5609 | > | |
| 5610 | <span>{triggerLabel}</span> | |
| 5611 | <ChevronDownIcon aria-hidden="true" className="size-3 opacity-60" /> | |
| 5612 | </MenuTrigger> | |
| 5613 | <MenuPopup align="start"> | |
| 5614 | {props.capabilities.supportsReasoning && ( | |
| 5615 | <MenuGroup> | |
| 5616 | <div className="px-2 py-1.5 font-medium text-muted-foreground text-xs">Reasoning</div> | |
| 5617 | <MenuRadioGroup | |
| 5618 | value={props.selection.reasoning} | |
| 5619 | onValueChange={(value) => { | |
| 5620 | if (props.disabled) return; | |
| 5621 | if (!value) return; | |
| 5622 | const nextReasoning = CURSOR_REASONING_OPTIONS.find((option) => option === value); | |
| 5623 | if (!nextReasoning) return; | |
| 5624 | props.onReasoningChange(nextReasoning); | |
| 5625 | }} | |
| 5626 | > | |
| 5627 | {CURSOR_REASONING_OPTIONS.map((reasoning) => ( | |
| 5628 | <MenuRadioItem key={reasoning} value={reasoning}> | |
| 5629 | {reasoning} | |
| 5630 | {reasoning === "normal" ? " (default)" : ""} | |
| 5631 | </MenuRadioItem> | |
| 5632 | ))} | |
| 5633 | </MenuRadioGroup> | |
| 5634 | </MenuGroup> | |
| 5635 | )} | |
| 5636 | {props.capabilities.supportsReasoning && | |
| 5637 | (props.capabilities.supportsFast || props.capabilities.supportsThinking) && ( | |
| 5638 | <MenuDivider /> | |
| 5639 | )} | |
| 5640 | {props.capabilities.supportsFast && ( | |
| 5641 | <MenuGroup> | |
| 5642 | <div className="px-2 py-1.5 font-medium text-muted-foreground text-xs">Fast Mode</div> | |
| 5643 | <MenuRadioGroup | |
| 5644 | value={props.selection.fast ? "on" : "off"} | |
| 5645 | onValueChange={(value) => { | |
| 5646 | if (props.disabled) return; | |
| 5647 | props.onFastModeChange(value === "on"); | |
| 5648 | }} | |
| 5649 | > | |
| 5650 | <MenuRadioItem value="off">off</MenuRadioItem> | |
| 5651 | <MenuRadioItem value="on">on</MenuRadioItem> | |
| 5652 | </MenuRadioGroup> | |
| 5653 | </MenuGroup> | |
| 5654 | )} | |
| 5655 | {props.capabilities.supportsFast && props.capabilities.supportsThinking && <MenuDivider />} | |
| 5656 | {props.capabilities.supportsThinking && ( | |
| 5657 | <MenuGroup> | |
| 5658 | <div className="px-2 py-1.5 font-medium text-muted-foreground text-xs">Thinking</div> | |
| 5659 | <MenuRadioGroup | |
| 5660 | value={props.selection.thinking ? "on" : "off"} | |
| 5661 | onValueChange={(value) => { | |
| 5662 | if (props.disabled) return; | |
| 5663 | props.onThinkingModeChange(value === "on"); | |
| 5664 | }} | |
| 5665 | > | |
| 5666 | <MenuRadioItem value="off">off</MenuRadioItem> | |
| 5667 | <MenuRadioItem value="on">on</MenuRadioItem> | |
| 5668 | </MenuRadioGroup> | |
| 5669 | </MenuGroup> | |
| 5670 | )} | |
| 5671 | </MenuPopup> | |
| 5672 | </Menu> | |
| 5673 | ); | |
| 5674 | }); | |
| 5675 | ||
| 5676 | 5401 | const OpenInPicker = memo(function OpenInPicker({ |
| 5677 | 5402 | keybindings, |
| 5678 | 5403 | availableEditors, |
apps/web/src/composerDraftStore.test.ts+3-3| ... | ... | @@ -394,15 +394,15 @@ describe("composerDraftStore setProvider", () => { |
| 394 | 394 | it("persists provider-only selection even when prompt/model are empty", () => { |
| 395 | 395 | const store = useComposerDraftStore.getState(); |
| 396 | 396 | |
| 397 | store.setProvider(threadId, "cursor"); | |
| 397 | store.setProvider(threadId, "codex"); | |
| 398 | 398 | |
| 399 | expect(useComposerDraftStore.getState().draftsByThreadId[threadId]?.provider).toBe("cursor"); | |
| 399 | expect(useComposerDraftStore.getState().draftsByThreadId[threadId]?.provider).toBe("codex"); | |
| 400 | 400 | }); |
| 401 | 401 | |
| 402 | 402 | it("removes empty provider-only draft when provider is reset", () => { |
| 403 | 403 | const store = useComposerDraftStore.getState(); |
| 404 | 404 | |
| 405 | store.setProvider(threadId, "cursor"); | |
| 405 | store.setProvider(threadId, "codex"); | |
| 406 | 406 | store.setProvider(threadId, null); |
| 407 | 407 | |
| 408 | 408 | expect(useComposerDraftStore.getState().draftsByThreadId[threadId]).toBeUndefined(); |
apps/web/src/composerDraftStore.ts+1-1| ... | ... | @@ -208,7 +208,7 @@ function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean { |
| 208 | 208 | } |
| 209 | 209 | |
| 210 | 210 | function normalizeProviderKind(value: unknown): ProviderKind | null { |
| 211 | return value === "codex" || value === "cursor" ? value : null; | |
| 211 | return value === "codex" ? value : null; | |
| 212 | 212 | } |
| 213 | 213 | |
| 214 | 214 | function revokeObjectPreviewUrl(previewUrl: string): void { |
apps/web/src/routes/_chat.settings.tsx-7| ... | ... | @@ -54,8 +54,6 @@ function getCustomModelsForProvider( |
| 54 | 54 | provider: ProviderKind, |
| 55 | 55 | ) { |
| 56 | 56 | switch (provider) { |
| 57 | case "cursor": | |
| 58 | return settings.customCursorModels; | |
| 59 | 57 | case "codex": |
| 60 | 58 | default: |
| 61 | 59 | return settings.customCodexModels; |
| ... | ... | @@ -67,8 +65,6 @@ function getDefaultCustomModelsForProvider( |
| 67 | 65 | provider: ProviderKind, |
| 68 | 66 | ) { |
| 69 | 67 | switch (provider) { |
| 70 | case "cursor": | |
| 71 | return defaults.customCursorModels; | |
| 72 | 68 | case "codex": |
| 73 | 69 | default: |
| 74 | 70 | return defaults.customCodexModels; |
| ... | ... | @@ -77,8 +73,6 @@ function getDefaultCustomModelsForProvider( |
| 77 | 73 | |
| 78 | 74 | function patchCustomModels(provider: ProviderKind, models: string[]) { |
| 79 | 75 | switch (provider) { |
| 80 | case "cursor": | |
| 81 | return { customCursorModels: models }; | |
| 82 | 76 | case "codex": |
| 83 | 77 | default: |
| 84 | 78 | return { customCodexModels: models }; |
| ... | ... | @@ -95,7 +89,6 @@ function SettingsRouteView() { |
| 95 | 89 | Record<ProviderKind, string> |
| 96 | 90 | >({ |
| 97 | 91 | codex: "", |
| 98 | cursor: "", | |
| 99 | 92 | }); |
| 100 | 93 | const [customModelErrorByProvider, setCustomModelErrorByProvider] = useState< |
| 101 | 94 | Partial<Record<ProviderKind, string | null>> |
apps/web/src/session-logic.ts+1-1| ... | ... | @@ -10,7 +10,7 @@ import { |
| 10 | 10 | |
| 11 | 11 | import type { ChatMessage, ProposedPlan, SessionPhase, ThreadSession, TurnDiffSummary } from "./types"; |
| 12 | 12 | |
| 13 | export type ProviderPickerKind = ProviderKind | "claudeCode"; | |
| 13 | export type ProviderPickerKind = ProviderKind | "claudeCode" | "cursor"; | |
| 14 | 14 | |
| 15 | 15 | export const PROVIDER_OPTIONS: Array<{ |
| 16 | 16 | value: ProviderPickerKind; |
apps/web/src/store.test.ts-23| ... | ... | @@ -141,27 +141,4 @@ describe("store read model sync", () => { |
| 141 | 141 | |
| 142 | 142 | expect(next.threads[0]?.model).toBe("gpt-5.3-codex"); |
| 143 | 143 | }); |
| 144 | ||
| 145 | it("resolves cursor aliases when session provider is cursor", () => { | |
| 146 | const initialState = makeState(makeThread()); | |
| 147 | const readModel = makeReadModel( | |
| 148 | makeReadModelThread({ | |
| 149 | model: "composer", | |
| 150 | session: { | |
| 151 | threadId: ThreadId.makeUnsafe("thread-1"), | |
| 152 | status: "ready", | |
| 153 | providerName: "cursor", | |
| 154 | runtimeMode: "approval-required", | |
| 155 | activeTurnId: null, | |
| 156 | lastError: null, | |
| 157 | updatedAt: "2026-02-27T00:00:00.000Z", | |
| 158 | }, | |
| 159 | }), | |
| 160 | ); | |
| 161 | ||
| 162 | const next = syncServerReadModel(initialState, readModel); | |
| 163 | ||
| 164 | expect(next.threads[0]?.model).toBe("composer-1.5"); | |
| 165 | expect(next.threads[0]?.session?.provider).toBe("cursor"); | |
| 166 | }); | |
| 167 | 144 | }); |
apps/web/src/store.ts+2-20| ... | ... | @@ -145,43 +145,25 @@ function toLegacySessionStatus( |
| 145 | 145 | } |
| 146 | 146 | |
| 147 | 147 | function toLegacyProvider(providerName: string | null): ProviderKind { |
| 148 | if (providerName === "codex" || providerName === "cursor") { | |
| 148 | if (providerName === "codex") { | |
| 149 | 149 | return providerName; |
| 150 | 150 | } |
| 151 | 151 | return "codex"; |
| 152 | 152 | } |
| 153 | 153 | |
| 154 | 154 | const CODEX_MODEL_SLUGS = new Set<string>(getModelOptions("codex").map((option) => option.slug)); |
| 155 | const CURSOR_MODEL_SLUGS = new Set<string>(getModelOptions("cursor").map((option) => option.slug)); | |
| 156 | const CURSOR_DISTINCT_MODEL_SLUGS = new Set( | |
| 157 | [...CURSOR_MODEL_SLUGS].filter((slug) => !CODEX_MODEL_SLUGS.has(slug)), | |
| 158 | ); | |
| 159 | 155 | |
| 160 | 156 | function inferProviderForThreadModel(input: { |
| 161 | 157 | readonly model: string; |
| 162 | 158 | readonly sessionProviderName: string | null; |
| 163 | 159 | }): ProviderKind { |
| 164 | if ( | |
| 165 | input.sessionProviderName === "codex" || | |
| 166 | input.sessionProviderName === "cursor" | |
| 167 | ) { | |
| 160 | if (input.sessionProviderName === "codex") { | |
| 168 | 161 | return input.sessionProviderName; |
| 169 | 162 | } |
| 170 | const normalizedCursor = normalizeModelSlug(input.model, "cursor"); | |
| 171 | if (normalizedCursor && CURSOR_DISTINCT_MODEL_SLUGS.has(normalizedCursor)) { | |
| 172 | return "cursor"; | |
| 173 | } | |
| 174 | 163 | const normalizedCodex = normalizeModelSlug(input.model, "codex"); |
| 175 | 164 | if (normalizedCodex && CODEX_MODEL_SLUGS.has(normalizedCodex)) { |
| 176 | 165 | return "codex"; |
| 177 | 166 | } |
| 178 | if ( | |
| 179 | input.model.trim().startsWith("composer-") || | |
| 180 | input.model.trim().startsWith("gemini-") || | |
| 181 | input.model.trim().endsWith("-thinking") | |
| 182 | ) { | |
| 183 | return "cursor"; | |
| 184 | } | |
| 185 | 167 | return "codex"; |
| 186 | 168 | } |
| 187 | 169 |
packages/contracts/src/model.ts+1-70| ... | ... | @@ -1,8 +1,5 @@ |
| 1 | 1 | import { Schema } from "effect"; |
| 2 | 2 | |
| 3 | export const CURSOR_REASONING_OPTIONS = ["low", "normal", "high", "xhigh"] as const; | |
| 4 | export type CursorReasoningOption = (typeof CURSOR_REASONING_OPTIONS)[number]; | |
| 5 | ||
| 6 | 3 | export const CODEX_REASONING_EFFORT_OPTIONS = ["xhigh", "high", "medium", "low"] as const; |
| 7 | 4 | export type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORT_OPTIONS)[number]; |
| 8 | 5 | |
| ... | ... | @@ -12,16 +9,8 @@ export const CodexModelOptions = Schema.Struct({ |
| 12 | 9 | }); |
| 13 | 10 | export type CodexModelOptions = typeof CodexModelOptions.Type; |
| 14 | 11 | |
| 15 | export const CursorModelOptions = Schema.Struct({ | |
| 16 | reasoning: Schema.optional(Schema.Literals(CURSOR_REASONING_OPTIONS)), | |
| 17 | fastMode: Schema.optional(Schema.Boolean), | |
| 18 | thinking: Schema.optional(Schema.Boolean), | |
| 19 | }); | |
| 20 | export type CursorModelOptions = typeof CursorModelOptions.Type; | |
| 21 | ||
| 22 | 12 | export const ProviderModelOptions = Schema.Struct({ |
| 23 | 13 | codex: Schema.optional(CodexModelOptions), |
| 24 | cursor: Schema.optional(CursorModelOptions), | |
| 25 | 14 | }); |
| 26 | 15 | export type ProviderModelOptions = typeof ProviderModelOptions.Type; |
| 27 | 16 | |
| ... | ... | @@ -30,25 +19,6 @@ type ModelOption = { |
| 30 | 19 | readonly name: string; |
| 31 | 20 | }; |
| 32 | 21 | |
| 33 | type CursorModelFamilyOption = { | |
| 34 | readonly slug: string; | |
| 35 | readonly name: string; | |
| 36 | }; | |
| 37 | ||
| 38 | export const CURSOR_MODEL_FAMILY_OPTIONS = [ | |
| 39 | { slug: "auto", name: "Auto" }, | |
| 40 | { slug: "composer-1.5", name: "Composer 1.5" }, | |
| 41 | { slug: "composer-1", name: "Composer 1" }, | |
| 42 | { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, | |
| 43 | { slug: "gpt-5.3-codex-spark-preview", name: "GPT-5.3 Codex Spark" }, | |
| 44 | { slug: "opus-4.6", name: "Claude 4.6 Opus" }, | |
| 45 | { slug: "opus-4.5", name: "Claude 4.5 Opus" }, | |
| 46 | { slug: "sonnet-4.6", name: "Claude 4.6 Sonnet" }, | |
| 47 | { slug: "gemini-3.1-pro", name: "Gemini 3.1 Pro" }, | |
| 48 | ] as const satisfies readonly CursorModelFamilyOption[]; | |
| 49 | ||
| 50 | export type CursorModelFamily = (typeof CURSOR_MODEL_FAMILY_OPTIONS)[number]["slug"]; | |
| 51 | ||
| 52 | 22 | export const MODEL_OPTIONS_BY_PROVIDER = { |
| 53 | 23 | codex: [ |
| 54 | 24 | { slug: "gpt-5.4", name: "GPT-5.4" }, |
| ... | ... | @@ -57,37 +27,14 @@ export const MODEL_OPTIONS_BY_PROVIDER = { |
| 57 | 27 | { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" }, |
| 58 | 28 | { slug: "gpt-5.2", name: "GPT-5.2" }, |
| 59 | 29 | ], |
| 60 | cursor: [ | |
| 61 | { slug: "auto", name: "Auto" }, | |
| 62 | { slug: "composer-1.5", name: "Composer 1.5" }, | |
| 63 | { slug: "composer-1", name: "Composer 1" }, | |
| 64 | { slug: "gpt-5.3-codex-low", name: "GPT-5.3 Codex Low" }, | |
| 65 | { slug: "gpt-5.3-codex-low-fast", name: "GPT-5.3 Codex Low Fast" }, | |
| 66 | { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, | |
| 67 | { slug: "gpt-5.3-codex-fast", name: "GPT-5.3 Codex Fast" }, | |
| 68 | { slug: "gpt-5.3-codex-high", name: "GPT-5.3 Codex High" }, | |
| 69 | { slug: "gpt-5.3-codex-high-fast", name: "GPT-5.3 Codex High Fast" }, | |
| 70 | { slug: "gpt-5.3-codex-xhigh", name: "GPT-5.3 Codex Extra High" }, | |
| 71 | { slug: "gpt-5.3-codex-xhigh-fast", name: "GPT-5.3 Codex Extra High Fast" }, | |
| 72 | { slug: "gpt-5.3-codex-spark-preview", name: "GPT-5.3 Codex Spark" }, | |
| 73 | { slug: "opus-4.6", name: "Claude 4.6 Opus" }, | |
| 74 | { slug: "opus-4.6-thinking", name: "Claude 4.6 Opus (Thinking)" }, | |
| 75 | { slug: "opus-4.5", name: "Claude 4.5 Opus" }, | |
| 76 | { slug: "opus-4.5-thinking", name: "Claude 4.5 Opus (Thinking)" }, | |
| 77 | { slug: "sonnet-4.6", name: "Claude 4.6 Sonnet" }, | |
| 78 | { slug: "sonnet-4.6-thinking", name: "Claude 4.6 Sonnet (Thinking)" }, | |
| 79 | { slug: "gemini-3.1-pro", name: "Gemini 3.1 Pro" }, | |
| 80 | ], | |
| 81 | } as const satisfies Record<"codex" | "cursor", readonly ModelOption[]>; | |
| 30 | } as const satisfies Record<"codex", readonly ModelOption[]>; | |
| 82 | 31 | |
| 83 | 32 | type BuiltInModelSlug = |
| 84 | 33 | (typeof MODEL_OPTIONS_BY_PROVIDER)[keyof typeof MODEL_OPTIONS_BY_PROVIDER][number]["slug"]; |
| 85 | 34 | export type ModelSlug = BuiltInModelSlug | (string & {}); |
| 86 | export type CursorModelSlug = (typeof MODEL_OPTIONS_BY_PROVIDER)["cursor"][number]["slug"]; | |
| 87 | 35 | |
| 88 | 36 | export const DEFAULT_MODEL_BY_PROVIDER = { |
| 89 | 37 | codex: "gpt-5.3-codex", |
| 90 | cursor: "opus-4.6-thinking", | |
| 91 | 38 | } as const satisfies Record<keyof typeof MODEL_OPTIONS_BY_PROVIDER, ModelSlug>; |
| 92 | 39 | |
| 93 | 40 | // Backward compatibility for existing Codex-only call sites. |
| ... | ... | @@ -102,25 +49,10 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER = { |
| 102 | 49 | "5.3-spark": "gpt-5.3-codex-spark", |
| 103 | 50 | "gpt-5.3-spark": "gpt-5.3-codex-spark", |
| 104 | 51 | }, |
| 105 | cursor: { | |
| 106 | composer: "composer-1.5", | |
| 107 | "composer-1.5": "composer-1.5", | |
| 108 | "composer-1": "composer-1", | |
| 109 | "gpt-5.3-codex": "gpt-5.3-codex", | |
| 110 | "gpt-5.3-codex-spark": "gpt-5.3-codex-spark-preview", | |
| 111 | "gemini-3.1-pro": "gemini-3.1-pro", | |
| 112 | "claude-4.6-sonnet-thinking": "sonnet-4.6-thinking", | |
| 113 | "claude-4.6-opus-thinking": "opus-4.6-thinking", | |
| 114 | "claude-4.5-opus-thinking": "opus-4.5-thinking", | |
| 115 | "sonnet-4.6-thinking": "sonnet-4.6-thinking", | |
| 116 | "opus-4.6-thinking": "opus-4.6-thinking", | |
| 117 | "opus-4.5-thinking": "opus-4.5-thinking", | |
| 118 | }, | |
| 119 | 52 | } as const satisfies Record<keyof typeof MODEL_OPTIONS_BY_PROVIDER, Record<string, ModelSlug>>; |
| 120 | 53 | |
| 121 | 54 | export const REASONING_EFFORT_OPTIONS_BY_PROVIDER = { |
| 122 | 55 | codex: CODEX_REASONING_EFFORT_OPTIONS, |
| 123 | cursor: [], | |
| 124 | 56 | } as const satisfies Record< |
| 125 | 57 | keyof typeof MODEL_OPTIONS_BY_PROVIDER, |
| 126 | 58 | readonly CodexReasoningEffort[] |
| ... | ... | @@ -128,5 +60,4 @@ export const REASONING_EFFORT_OPTIONS_BY_PROVIDER = { |
| 128 | 60 | |
| 129 | 61 | export const DEFAULT_REASONING_EFFORT_BY_PROVIDER = { |
| 130 | 62 | codex: "high", |
| 131 | cursor: null, | |
| 132 | 63 | } as const satisfies Record<keyof typeof MODEL_OPTIONS_BY_PROVIDER, CodexReasoningEffort | null>; |
packages/contracts/src/orchestration.test.ts+2-22| ... | ... | @@ -128,11 +128,11 @@ it.effect("preserves explicit provider and runtime mode in thread.turn.start", ( |
| 128 | 128 | text: "hello", |
| 129 | 129 | attachments: [], |
| 130 | 130 | }, |
| 131 | provider: "cursor", | |
| 131 | provider: "codex", | |
| 132 | 132 | runtimeMode: "full-access", |
| 133 | 133 | createdAt: "2026-01-01T00:00:00.000Z", |
| 134 | 134 | }); |
| 135 | assert.strictEqual(parsed.provider, "cursor"); | |
| 135 | assert.strictEqual(parsed.provider, "codex"); | |
| 136 | 136 | assert.strictEqual(parsed.runtimeMode, "full-access"); |
| 137 | 137 | assert.strictEqual(parsed.interactionMode, DEFAULT_PROVIDER_INTERACTION_MODE); |
| 138 | 138 | }), |
| ... | ... | @@ -166,26 +166,6 @@ it.effect("accepts provider-scoped model options in thread.turn.start", () => |
| 166 | 166 | }), |
| 167 | 167 | ); |
| 168 | 168 | |
| 169 | it.effect("accepts cursor provider in thread.turn.start", () => | |
| 170 | Effect.gen(function* () { | |
| 171 | const parsed = yield* decodeThreadTurnStartCommand({ | |
| 172 | type: "thread.turn.start", | |
| 173 | commandId: "cmd-turn-cursor", | |
| 174 | threadId: "thread-1", | |
| 175 | message: { | |
| 176 | messageId: "msg-3", | |
| 177 | role: "user", | |
| 178 | text: "hello", | |
| 179 | attachments: [], | |
| 180 | }, | |
| 181 | provider: "cursor", | |
| 182 | createdAt: "2026-01-01T00:00:00.000Z", | |
| 183 | }); | |
| 184 | assert.strictEqual(parsed.provider, "cursor"); | |
| 185 | assert.strictEqual(parsed.interactionMode, DEFAULT_PROVIDER_INTERACTION_MODE); | |
| 186 | }), | |
| 187 | ); | |
| 188 | ||
| 189 | 169 | it.effect( |
| 190 | 170 | "decodes thread.turn-start-requested defaults for provider, runtime mode, and interaction mode", |
| 191 | 171 | () => |
packages/contracts/src/orchestration.ts+1-1| ... | ... | @@ -27,7 +27,7 @@ export const ORCHESTRATION_WS_CHANNELS = { |
| 27 | 27 | domainEvent: "orchestration.domainEvent", |
| 28 | 28 | } as const; |
| 29 | 29 | |
| 30 | export const ProviderKind = Schema.Literals(["codex", "cursor"]); | |
| 30 | export const ProviderKind = Schema.Literal("codex"); | |
| 31 | 31 | export type ProviderKind = typeof ProviderKind.Type; |
| 32 | 32 | export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex"; |
| 33 | 33 | export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); |
packages/contracts/src/provider.test.ts-25| ... | ... | @@ -42,31 +42,6 @@ describe("ProviderSessionStartInput", () => { |
| 42 | 42 | }), |
| 43 | 43 | ).toThrow(); |
| 44 | 44 | }); |
| 45 | ||
| 46 | it("accepts cursor provider payloads", () => { | |
| 47 | const parsed = decodeProviderSessionStartInput({ | |
| 48 | threadId: "thread-1", | |
| 49 | provider: "cursor", | |
| 50 | cwd: "/tmp/workspace", | |
| 51 | model: "composer-1.5", | |
| 52 | modelOptions: { | |
| 53 | cursor: { | |
| 54 | thinking: true, | |
| 55 | }, | |
| 56 | }, | |
| 57 | providerOptions: { | |
| 58 | cursor: { | |
| 59 | binaryPath: "/usr/local/bin/agent", | |
| 60 | }, | |
| 61 | }, | |
| 62 | runtimeMode: "approval-required", | |
| 63 | }); | |
| 64 | expect(parsed.provider).toBe("cursor"); | |
| 65 | expect(parsed.model).toBe("composer-1.5"); | |
| 66 | expect(parsed.modelOptions?.cursor?.thinking).toBe(true); | |
| 67 | expect(parsed.providerOptions?.cursor?.binaryPath).toBe("/usr/local/bin/agent"); | |
| 68 | expect(parsed.runtimeMode).toBe("approval-required"); | |
| 69 | }); | |
| 70 | 45 | }); |
| 71 | 46 | |
| 72 | 47 | describe("ProviderSendTurnInput", () => { |
packages/contracts/src/provider.ts-5| ... | ... | @@ -50,13 +50,8 @@ const CodexProviderStartOptions = Schema.Struct({ |
| 50 | 50 | homePath: Schema.optional(TrimmedNonEmptyStringSchema), |
| 51 | 51 | }); |
| 52 | 52 | |
| 53 | const CursorProviderStartOptions = Schema.Struct({ | |
| 54 | binaryPath: Schema.optional(TrimmedNonEmptyStringSchema), | |
| 55 | }); | |
| 56 | ||
| 57 | 53 | const ProviderStartOptions = Schema.Struct({ |
| 58 | 54 | codex: Schema.optional(CodexProviderStartOptions), |
| 59 | cursor: Schema.optional(CursorProviderStartOptions), | |
| 60 | 55 | }); |
| 61 | 56 | |
| 62 | 57 | export const ProviderSessionStartInput = Schema.Struct({ |
packages/contracts/src/providerRuntime.ts-3| ... | ... | @@ -20,9 +20,6 @@ const RuntimeEventRawSource = Schema.Literals([ |
| 20 | 20 | "codex.app-server.request", |
| 21 | 21 | "codex.eventmsg", |
| 22 | 22 | "codex.sdk.thread-event", |
| 23 | "cursor.acp.notification", | |
| 24 | "cursor.acp.request", | |
| 25 | "cursor.acp.response", | |
| 26 | 23 | ]); |
| 27 | 24 | export type RuntimeEventRawSource = typeof RuntimeEventRawSource.Type; |
| 28 | 25 |
packages/shared/src/model.test.ts+1-89| ... | ... | @@ -1,24 +1,13 @@ |
| 1 | 1 | import { describe, expect, it } from "vitest"; |
| 2 | import { | |
| 3 | CURSOR_MODEL_FAMILY_OPTIONS, | |
| 4 | CURSOR_REASONING_OPTIONS, | |
| 5 | DEFAULT_MODEL, | |
| 6 | DEFAULT_MODEL_BY_PROVIDER, | |
| 7 | MODEL_OPTIONS, | |
| 8 | MODEL_OPTIONS_BY_PROVIDER, | |
| 9 | } from "../../contracts/src"; | |
| 2 | import { DEFAULT_MODEL, MODEL_OPTIONS } from "@t3tools/contracts"; | |
| 10 | 3 | |
| 11 | 4 | import { |
| 12 | 5 | getDefaultModel, |
| 13 | 6 | getDefaultReasoningEffort, |
| 14 | getCursorModelFamilyOptions, | |
| 15 | 7 | getModelOptions, |
| 16 | 8 | getReasoningEffortOptions, |
| 17 | 9 | normalizeModelSlug, |
| 18 | parseCursorModelSelection, | |
| 19 | resolveCursorModelFromSelection, | |
| 20 | 10 | resolveModelSlug, |
| 21 | resolveModelSlugForProvider, | |
| 22 | 11 | } from "./model"; |
| 23 | 12 | |
| 24 | 13 | describe("normalizeModelSlug", () => { |
| ... | ... | @@ -38,17 +27,6 @@ describe("normalizeModelSlug", () => { |
| 38 | 27 | expect(normalizeModelSlug("gpt-5.2")).toBe("gpt-5.2"); |
| 39 | 28 | expect(normalizeModelSlug("gpt-5.2-codex")).toBe("gpt-5.2-codex"); |
| 40 | 29 | }); |
| 41 | ||
| 42 | it("uses provider-specific aliases", () => { | |
| 43 | expect(normalizeModelSlug("composer", "cursor")).toBe("composer-1.5"); | |
| 44 | expect(normalizeModelSlug("gpt-5.3-codex-spark", "cursor")).toBe( | |
| 45 | "gpt-5.3-codex-spark-preview", | |
| 46 | ); | |
| 47 | expect(normalizeModelSlug("gemini-3.1", "cursor")).toBe("gemini-3.1"); | |
| 48 | expect(normalizeModelSlug("claude-4.6-sonnet-thinking", "cursor")).toBe( | |
| 49 | "sonnet-4.6-thinking", | |
| 50 | ); | |
| 51 | }); | |
| 52 | 30 | }); |
| 53 | 31 | |
| 54 | 32 | describe("resolveModelSlug", () => { |
| ... | ... | @@ -67,70 +45,9 @@ describe("resolveModelSlug", () => { |
| 67 | 45 | expect(resolveModelSlug(model.slug)).toBe(model.slug); |
| 68 | 46 | } |
| 69 | 47 | }); |
| 70 | ||
| 71 | it("supports provider-aware resolution", () => { | |
| 72 | expect(resolveModelSlugForProvider("cursor", undefined)).toBe( | |
| 73 | DEFAULT_MODEL_BY_PROVIDER.cursor, | |
| 74 | ); | |
| 75 | expect(resolveModelSlugForProvider("cursor", "composer")).toBe("composer-1.5"); | |
| 76 | expect(resolveModelSlugForProvider("cursor", "gpt-5.3-codex-high-fast")).toBe( | |
| 77 | "gpt-5.3-codex-high-fast", | |
| 78 | ); | |
| 79 | expect(resolveModelSlugForProvider("cursor", "claude-sonnet-4-6")).toBe( | |
| 80 | DEFAULT_MODEL_BY_PROVIDER.cursor, | |
| 81 | ); | |
| 82 | }); | |
| 83 | ||
| 84 | 48 | it("keeps codex defaults for backward compatibility", () => { |
| 85 | 49 | expect(getDefaultModel()).toBe(DEFAULT_MODEL); |
| 86 | 50 | expect(getModelOptions()).toEqual(MODEL_OPTIONS); |
| 87 | expect(getModelOptions("cursor")).toEqual(MODEL_OPTIONS_BY_PROVIDER.cursor); | |
| 88 | expect(getCursorModelFamilyOptions()).toEqual(CURSOR_MODEL_FAMILY_OPTIONS); | |
| 89 | }); | |
| 90 | }); | |
| 91 | ||
| 92 | describe("cursor model selection", () => { | |
| 93 | it("includes the expected cursor reasoning levels and families", () => { | |
| 94 | expect(CURSOR_REASONING_OPTIONS).toEqual(["low", "normal", "high", "xhigh"]); | |
| 95 | expect(getCursorModelFamilyOptions().map((option) => option.slug)).toContain("gpt-5.3-codex"); | |
| 96 | expect(getCursorModelFamilyOptions().map((option) => option.slug)).toContain("opus-4.6"); | |
| 97 | }); | |
| 98 | ||
| 99 | it("parses codex reasoning and fast mode variants", () => { | |
| 100 | expect(parseCursorModelSelection("gpt-5.3-codex-high-fast")).toEqual({ | |
| 101 | family: "gpt-5.3-codex", | |
| 102 | reasoning: "high", | |
| 103 | fast: true, | |
| 104 | thinking: false, | |
| 105 | }); | |
| 106 | expect(parseCursorModelSelection("gpt-5.2-codex")).toEqual( | |
| 107 | parseCursorModelSelection(DEFAULT_MODEL_BY_PROVIDER.cursor), | |
| 108 | ); | |
| 109 | }); | |
| 110 | ||
| 111 | it("parses and resolves thinking variants", () => { | |
| 112 | expect(parseCursorModelSelection("sonnet-4.6-thinking")).toEqual({ | |
| 113 | family: "sonnet-4.6", | |
| 114 | reasoning: "normal", | |
| 115 | fast: false, | |
| 116 | thinking: true, | |
| 117 | }); | |
| 118 | expect( | |
| 119 | resolveCursorModelFromSelection({ | |
| 120 | family: "sonnet-4.6", | |
| 121 | thinking: true, | |
| 122 | }), | |
| 123 | ).toBe("sonnet-4.6-thinking"); | |
| 124 | }); | |
| 125 | ||
| 126 | it("resolves codex family selections into concrete model ids", () => { | |
| 127 | expect( | |
| 128 | resolveCursorModelFromSelection({ | |
| 129 | family: "gpt-5.3-codex", | |
| 130 | reasoning: "xhigh", | |
| 131 | fast: true, | |
| 132 | }), | |
| 133 | ).toBe("gpt-5.3-codex-xhigh-fast"); | |
| 134 | 51 | }); |
| 135 | 52 | }); |
| 136 | 53 | |
| ... | ... | @@ -138,15 +55,10 @@ describe("getReasoningEffortOptions", () => { |
| 138 | 55 | it("returns codex reasoning options for codex", () => { |
| 139 | 56 | expect(getReasoningEffortOptions("codex")).toEqual(["xhigh", "high", "medium", "low"]); |
| 140 | 57 | }); |
| 141 | ||
| 142 | it("returns no reasoning options for cursor", () => { | |
| 143 | expect(getReasoningEffortOptions("cursor")).toEqual([]); | |
| 144 | }); | |
| 145 | 58 | }); |
| 146 | 59 | |
| 147 | 60 | describe("getDefaultReasoningEffort", () => { |
| 148 | 61 | it("returns provider-scoped defaults", () => { |
| 149 | 62 | expect(getDefaultReasoningEffort("codex")).toBe("high"); |
| 150 | expect(getDefaultReasoningEffort("cursor")).toBeNull(); | |
| 151 | 63 | }); |
| 152 | 64 | }); |
packages/shared/src/model.ts+6-219| ... | ... | @@ -1,233 +1,26 @@ |
| 1 | 1 | import { |
| 2 | 2 | CODEX_REASONING_EFFORT_OPTIONS, |
| 3 | CURSOR_MODEL_FAMILY_OPTIONS, | |
| 4 | CURSOR_REASONING_OPTIONS, | |
| 5 | 3 | DEFAULT_MODEL, |
| 6 | 4 | DEFAULT_MODEL_BY_PROVIDER, |
| 7 | 5 | MODEL_OPTIONS_BY_PROVIDER, |
| 8 | 6 | MODEL_SLUG_ALIASES_BY_PROVIDER, |
| 9 | 7 | type CodexReasoningEffort, |
| 10 | type CursorModelFamily, | |
| 11 | type CursorModelSlug, | |
| 12 | type CursorReasoningOption, | |
| 13 | 8 | type ModelSlug, |
| 14 | 9 | type ProviderKind, |
| 15 | } from "../../contracts/src"; | |
| 10 | } from "@t3tools/contracts"; | |
| 16 | 11 | |
| 17 | 12 | type CatalogProvider = keyof typeof MODEL_OPTIONS_BY_PROVIDER; |
| 18 | 13 | |
| 19 | type CursorModelCapability = { | |
| 20 | readonly supportsReasoning: boolean; | |
| 21 | readonly supportsFast: boolean; | |
| 22 | readonly supportsThinking: boolean; | |
| 23 | readonly defaultReasoning: CursorReasoningOption; | |
| 24 | readonly defaultThinking: boolean; | |
| 25 | }; | |
| 26 | ||
| 27 | const CURSOR_MODEL_CAPABILITY_BY_FAMILY: Record<CursorModelFamily, CursorModelCapability> = { | |
| 28 | auto: { | |
| 29 | supportsReasoning: false, | |
| 30 | supportsFast: false, | |
| 31 | supportsThinking: false, | |
| 32 | defaultReasoning: "normal", | |
| 33 | defaultThinking: false, | |
| 34 | }, | |
| 35 | "composer-1.5": { | |
| 36 | supportsReasoning: false, | |
| 37 | supportsFast: false, | |
| 38 | supportsThinking: false, | |
| 39 | defaultReasoning: "normal", | |
| 40 | defaultThinking: false, | |
| 41 | }, | |
| 42 | "composer-1": { | |
| 43 | supportsReasoning: false, | |
| 44 | supportsFast: false, | |
| 45 | supportsThinking: false, | |
| 46 | defaultReasoning: "normal", | |
| 47 | defaultThinking: false, | |
| 48 | }, | |
| 49 | "gpt-5.3-codex": { | |
| 50 | supportsReasoning: true, | |
| 51 | supportsFast: true, | |
| 52 | supportsThinking: false, | |
| 53 | defaultReasoning: "normal", | |
| 54 | defaultThinking: false, | |
| 55 | }, | |
| 56 | "gpt-5.3-codex-spark-preview": { | |
| 57 | supportsReasoning: false, | |
| 58 | supportsFast: false, | |
| 59 | supportsThinking: false, | |
| 60 | defaultReasoning: "normal", | |
| 61 | defaultThinking: false, | |
| 62 | }, | |
| 63 | "opus-4.6": { | |
| 64 | supportsReasoning: false, | |
| 65 | supportsFast: false, | |
| 66 | supportsThinking: true, | |
| 67 | defaultReasoning: "normal", | |
| 68 | defaultThinking: true, | |
| 69 | }, | |
| 70 | "opus-4.5": { | |
| 71 | supportsReasoning: false, | |
| 72 | supportsFast: false, | |
| 73 | supportsThinking: true, | |
| 74 | defaultReasoning: "normal", | |
| 75 | defaultThinking: true, | |
| 76 | }, | |
| 77 | "sonnet-4.6": { | |
| 78 | supportsReasoning: false, | |
| 79 | supportsFast: false, | |
| 80 | supportsThinking: true, | |
| 81 | defaultReasoning: "normal", | |
| 82 | defaultThinking: true, | |
| 83 | }, | |
| 84 | "gemini-3.1-pro": { | |
| 85 | supportsReasoning: false, | |
| 86 | supportsFast: false, | |
| 87 | supportsThinking: false, | |
| 88 | defaultReasoning: "normal", | |
| 89 | defaultThinking: false, | |
| 90 | }, | |
| 91 | }; | |
| 92 | ||
| 93 | function hasModelCatalog(provider: ProviderKind): provider is CatalogProvider { | |
| 94 | return Object.hasOwn(MODEL_OPTIONS_BY_PROVIDER, provider); | |
| 95 | } | |
| 96 | ||
| 97 | 14 | const MODEL_SLUG_SET_BY_PROVIDER: Record<CatalogProvider, ReadonlySet<ModelSlug>> = { |
| 98 | 15 | codex: new Set(MODEL_OPTIONS_BY_PROVIDER.codex.map((option) => option.slug)), |
| 99 | cursor: new Set(MODEL_OPTIONS_BY_PROVIDER.cursor.map((option) => option.slug)), | |
| 100 | 16 | }; |
| 101 | 17 | |
| 102 | const CURSOR_MODEL_FAMILY_SET = new Set<CursorModelFamily>( | |
| 103 | CURSOR_MODEL_FAMILY_OPTIONS.map((option) => option.slug), | |
| 104 | ); | |
| 105 | ||
| 106 | export interface CursorModelSelection { | |
| 107 | readonly family: CursorModelFamily; | |
| 108 | readonly reasoning: CursorReasoningOption; | |
| 109 | readonly fast: boolean; | |
| 110 | readonly thinking: boolean; | |
| 111 | } | |
| 112 | ||
| 113 | 18 | export function getModelOptions(provider: ProviderKind = "codex") { |
| 114 | return hasModelCatalog(provider) ? MODEL_OPTIONS_BY_PROVIDER[provider] : []; | |
| 115 | } | |
| 116 | ||
| 117 | export function getCursorModelFamilyOptions() { | |
| 118 | return CURSOR_MODEL_FAMILY_OPTIONS; | |
| 119 | } | |
| 120 | ||
| 121 | export function getCursorModelCapabilities(family: CursorModelFamily) { | |
| 122 | return CURSOR_MODEL_CAPABILITY_BY_FAMILY[family]; | |
| 123 | } | |
| 124 | ||
| 125 | function fallbackCursorModelFamily(): CursorModelFamily { | |
| 126 | const fallback = parseCursorModelSelection(DEFAULT_MODEL_BY_PROVIDER.cursor); | |
| 127 | return fallback.family; | |
| 128 | } | |
| 129 | ||
| 130 | function resolveCursorModelFamily(model: string | null | undefined): CursorModelFamily { | |
| 131 | const normalized = normalizeModelSlug(model, "cursor"); | |
| 132 | if (!normalized) { | |
| 133 | return fallbackCursorModelFamily(); | |
| 134 | } | |
| 135 | ||
| 136 | if ( | |
| 137 | normalized === "gpt-5.3-codex" || | |
| 138 | normalized === "gpt-5.3-codex-fast" || | |
| 139 | normalized === "gpt-5.3-codex-low" || | |
| 140 | normalized === "gpt-5.3-codex-low-fast" || | |
| 141 | normalized === "gpt-5.3-codex-high" || | |
| 142 | normalized === "gpt-5.3-codex-high-fast" || | |
| 143 | normalized === "gpt-5.3-codex-xhigh" || | |
| 144 | normalized === "gpt-5.3-codex-xhigh-fast" | |
| 145 | ) { | |
| 146 | return "gpt-5.3-codex"; | |
| 147 | } | |
| 148 | ||
| 149 | if (normalized === "sonnet-4.6-thinking") { | |
| 150 | return "sonnet-4.6"; | |
| 151 | } | |
| 152 | if (normalized === "opus-4.6-thinking") { | |
| 153 | return "opus-4.6"; | |
| 154 | } | |
| 155 | if (normalized === "opus-4.5-thinking") { | |
| 156 | return "opus-4.5"; | |
| 157 | } | |
| 158 | ||
| 159 | return CURSOR_MODEL_FAMILY_SET.has(normalized as CursorModelFamily) | |
| 160 | ? (normalized as CursorModelFamily) | |
| 161 | : fallbackCursorModelFamily(); | |
| 162 | } | |
| 163 | ||
| 164 | function resolveCursorReasoning(model: CursorModelSlug): CursorReasoningOption { | |
| 165 | if (model.includes("-xhigh")) return "xhigh"; | |
| 166 | if (model.includes("-high")) return "high"; | |
| 167 | if (model.includes("-low")) return "low"; | |
| 168 | return "normal"; | |
| 169 | } | |
| 170 | ||
| 171 | export function parseCursorModelSelection(model: string | null | undefined): CursorModelSelection { | |
| 172 | const family = resolveCursorModelFamily(model); | |
| 173 | const capability = CURSOR_MODEL_CAPABILITY_BY_FAMILY[family]; | |
| 174 | const normalized = resolveModelSlugForProvider("cursor", model) as CursorModelSlug; | |
| 175 | ||
| 176 | if (capability.supportsReasoning) { | |
| 177 | return { | |
| 178 | family, | |
| 179 | reasoning: resolveCursorReasoning(normalized), | |
| 180 | fast: normalized.endsWith("-fast"), | |
| 181 | thinking: false, | |
| 182 | }; | |
| 183 | } | |
| 184 | ||
| 185 | if (capability.supportsThinking) { | |
| 186 | return { | |
| 187 | family, | |
| 188 | reasoning: capability.defaultReasoning, | |
| 189 | fast: false, | |
| 190 | thinking: normalized.endsWith("-thinking"), | |
| 191 | }; | |
| 192 | } | |
| 193 | ||
| 194 | return { | |
| 195 | family, | |
| 196 | reasoning: capability.defaultReasoning, | |
| 197 | fast: false, | |
| 198 | thinking: capability.defaultThinking, | |
| 199 | }; | |
| 200 | } | |
| 201 | ||
| 202 | export function resolveCursorModelFromSelection(input: { | |
| 203 | readonly family: CursorModelFamily; | |
| 204 | readonly reasoning?: CursorReasoningOption | null; | |
| 205 | readonly fast?: boolean; | |
| 206 | readonly thinking?: boolean; | |
| 207 | }): CursorModelSlug { | |
| 208 | const family = resolveCursorModelFamily(input.family); | |
| 209 | const capability = CURSOR_MODEL_CAPABILITY_BY_FAMILY[family]; | |
| 210 | ||
| 211 | if (capability.supportsReasoning) { | |
| 212 | const reasoning = CURSOR_REASONING_OPTIONS.includes(input.reasoning ?? "normal") | |
| 213 | ? (input.reasoning ?? "normal") | |
| 214 | : capability.defaultReasoning; | |
| 215 | const reasoningSuffix = reasoning === "normal" ? "" : `-${reasoning}`; | |
| 216 | const fastSuffix = input.fast ? "-fast" : ""; | |
| 217 | const candidate = `${family}${reasoningSuffix}${fastSuffix}`; | |
| 218 | return resolveModelSlugForProvider("cursor", candidate) as CursorModelSlug; | |
| 219 | } | |
| 220 | ||
| 221 | if (capability.supportsThinking) { | |
| 222 | const candidate = input.thinking ? `${family}-thinking` : family; | |
| 223 | return resolveModelSlugForProvider("cursor", candidate) as CursorModelSlug; | |
| 224 | } | |
| 225 | ||
| 226 | return resolveModelSlugForProvider("cursor", family) as CursorModelSlug; | |
| 19 | return MODEL_OPTIONS_BY_PROVIDER[provider]; | |
| 227 | 20 | } |
| 228 | 21 | |
| 229 | 22 | export function getDefaultModel(provider: ProviderKind = "codex"): ModelSlug { |
| 230 | return hasModelCatalog(provider) ? DEFAULT_MODEL_BY_PROVIDER[provider] : DEFAULT_MODEL; | |
| 23 | return DEFAULT_MODEL_BY_PROVIDER[provider] ?? DEFAULT_MODEL; | |
| 231 | 24 | } |
| 232 | 25 | |
| 233 | 26 | export function normalizeModelSlug( |
| ... | ... | @@ -243,10 +36,6 @@ export function normalizeModelSlug( |
| 243 | 36 | return null; |
| 244 | 37 | } |
| 245 | 38 | |
| 246 | if (!hasModelCatalog(provider)) { | |
| 247 | return trimmed as ModelSlug; | |
| 248 | } | |
| 249 | ||
| 250 | 39 | const aliases = MODEL_SLUG_ALIASES_BY_PROVIDER[provider] as Record<string, ModelSlug>; |
| 251 | 40 | return aliases[trimmed] ?? (trimmed as ModelSlug); |
| 252 | 41 | } |
| ... | ... | @@ -260,11 +49,9 @@ export function resolveModelSlug( |
| 260 | 49 | return getDefaultModel(provider); |
| 261 | 50 | } |
| 262 | 51 | |
| 263 | if (!hasModelCatalog(provider)) { | |
| 264 | return getDefaultModel(provider); | |
| 265 | } | |
| 266 | ||
| 267 | return MODEL_SLUG_SET_BY_PROVIDER[provider].has(normalized) ? normalized : getDefaultModel(provider); | |
| 52 | return MODEL_SLUG_SET_BY_PROVIDER[provider].has(normalized) | |
| 53 | ? normalized | |
| 54 | : getDefaultModel(provider); | |
| 268 | 55 | } |
| 269 | 56 | |
| 270 | 57 | export function resolveModelSlugForProvider( |