| 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,7 +188,7 @@ export interface OrchestrationIntegrationHarness { |
| 188 | } | 188 | } |
| 189 | 189 | ||
| 190 | interface MakeOrchestrationIntegrationHarnessOptions { | 190 | interface MakeOrchestrationIntegrationHarnessOptions { |
| 191 | readonly provider?: "codex" | "cursor"; | 191 | readonly provider?: "codex"; |
| 192 | readonly realCodex?: boolean; | 192 | readonly realCodex?: boolean; |
| 193 | } | 193 | } |
| 194 | 194 |
apps/server/integration/TestProviderAdapter.integration.ts+5-5| ... | @@ -35,7 +35,7 @@ export interface TestTurnResponse { | ... | @@ -35,7 +35,7 @@ export interface TestTurnResponse { |
| 35 | export type FixtureProviderRuntimeEvent = { | 35 | export type FixtureProviderRuntimeEvent = { |
| 36 | readonly type: string; | 36 | readonly type: string; |
| 37 | readonly eventId: EventId; | 37 | readonly eventId: EventId; |
| 38 | readonly provider: "codex" | "cursor"; | 38 | readonly provider: "codex"; |
| 39 | readonly createdAt: string; | 39 | readonly createdAt: string; |
| 40 | readonly threadId: string; | 40 | readonly threadId: string; |
| 41 | readonly turnId?: string | undefined; | 41 | readonly turnId?: string | undefined; |
| ... | @@ -178,7 +178,7 @@ function normalizeFixtureEvent(rawEvent: Record<string, unknown>): ProviderRunti | ... | @@ -178,7 +178,7 @@ function normalizeFixtureEvent(rawEvent: Record<string, unknown>): ProviderRunti |
| 178 | 178 | ||
| 179 | export interface TestProviderAdapterHarness { | 179 | export interface TestProviderAdapterHarness { |
| 180 | readonly adapter: ProviderAdapterShape<ProviderAdapterError>; | 180 | readonly adapter: ProviderAdapterShape<ProviderAdapterError>; |
| 181 | readonly provider: "codex" | "cursor"; | 181 | readonly provider: "codex"; |
| 182 | readonly queueTurnResponse: ( | 182 | readonly queueTurnResponse: ( |
| 183 | threadId: ThreadId, | 183 | threadId: ThreadId, |
| 184 | response: TestTurnResponse, | 184 | response: TestTurnResponse, |
| ... | @@ -198,7 +198,7 @@ export interface TestProviderAdapterHarness { | ... | @@ -198,7 +198,7 @@ export interface TestProviderAdapterHarness { |
| 198 | } | 198 | } |
| 199 | 199 | ||
| 200 | interface MakeTestProviderAdapterHarnessOptions { | 200 | interface MakeTestProviderAdapterHarnessOptions { |
| 201 | readonly provider?: "codex" | "cursor"; | 201 | readonly provider?: "codex"; |
| 202 | } | 202 | } |
| 203 | 203 | ||
| 204 | function nowIso(): string { | 204 | function nowIso(): string { |
| ... | @@ -206,7 +206,7 @@ function nowIso(): string { | ... | @@ -206,7 +206,7 @@ function nowIso(): string { |
| 206 | } | 206 | } |
| 207 | 207 | ||
| 208 | function sessionNotFound( | 208 | function sessionNotFound( |
| 209 | provider: "codex" | "cursor", | 209 | provider: "codex", |
| 210 | threadId: ThreadId, | 210 | threadId: ThreadId, |
| 211 | ): ProviderAdapterSessionNotFoundError { | 211 | ): ProviderAdapterSessionNotFoundError { |
| 212 | return new ProviderAdapterSessionNotFoundError({ | 212 | return new ProviderAdapterSessionNotFoundError({ |
| ... | @@ -216,7 +216,7 @@ function sessionNotFound( | ... | @@ -216,7 +216,7 @@ function sessionNotFound( |
| 216 | } | 216 | } |
| 217 | 217 | ||
| 218 | function missingSessionEffect( | 218 | function missingSessionEffect( |
| 219 | provider: "codex" | "cursor", | 219 | provider: "codex", |
| 220 | threadId: ThreadId, | 220 | threadId: ThreadId, |
| 221 | ): Effect.Effect<never, ProviderAdapterError> { | 221 | ): Effect.Effect<never, ProviderAdapterError> { |
| 222 | return Effect.fail(sessionNotFound(provider, threadId)); | 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,7 +32,7 @@ const PROJECT_ID = asProjectId("project-1"); |
| 32 | const THREAD_ID = ThreadId.makeUnsafe("thread-1"); | 32 | const THREAD_ID = ThreadId.makeUnsafe("thread-1"); |
| 33 | const FIXTURE_TURN_ID = "fixture-turn"; | 33 | const FIXTURE_TURN_ID = "fixture-turn"; |
| 34 | const APPROVAL_REQUEST_ID = asApprovalRequestId("req-approval-1"); | 34 | const APPROVAL_REQUEST_ID = asApprovalRequestId("req-approval-1"); |
| 35 | type IntegrationProvider = "codex" | "cursor"; | 35 | type IntegrationProvider = "codex"; |
| 36 | 36 | ||
| 37 | function nowIso() { | 37 | function nowIso() { |
| 38 | return new Date().toISOString(); | 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,7 +43,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); |
| 43 | type LegacyProviderRuntimeEvent = { | 43 | type LegacyProviderRuntimeEvent = { |
| 44 | readonly type: string; | 44 | readonly type: string; |
| 45 | readonly eventId: EventId; | 45 | readonly eventId: EventId; |
| 46 | readonly provider: "codex" | "cursor"; | 46 | readonly provider: "codex"; |
| 47 | readonly createdAt: string; | 47 | readonly createdAt: string; |
| 48 | readonly threadId: ThreadId; | 48 | readonly threadId: ThreadId; |
| 49 | readonly turnId?: string | undefined; | 49 | readonly turnId?: string | undefined; |
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+4-147| ... | @@ -93,8 +93,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -93,8 +93,7 @@ describe("ProviderCommandReactor", () => { |
| 93 | typeof input === "object" && | 93 | typeof input === "object" && |
| 94 | input !== null && | 94 | input !== null && |
| 95 | "provider" in input && | 95 | "provider" in input && |
| 96 | (input.provider === "codex" || | 96 | input.provider === "codex" |
| 97 | input.provider === "cursor") | ||
| 98 | ? input.provider | 97 | ? input.provider |
| 99 | : "codex"; | 98 | : "codex"; |
| 100 | const resumeCursor = | 99 | const resumeCursor = |
| ... | @@ -186,7 +185,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -186,7 +185,7 @@ describe("ProviderCommandReactor", () => { |
| 186 | listSessions: () => Effect.succeed(runtimeSessions), | 185 | listSessions: () => Effect.succeed(runtimeSessions), |
| 187 | getCapabilities: (provider) => | 186 | getCapabilities: (provider) => |
| 188 | Effect.succeed({ | 187 | Effect.succeed({ |
| 189 | sessionModelSwitch: provider === "cursor" ? "unsupported" : "in-session", | 188 | sessionModelSwitch: provider === "codex" ? "in-session" : "in-session", |
| 190 | }), | 189 | }), |
| 191 | rollbackConversation: () => unsupported(), | 190 | rollbackConversation: () => unsupported(), |
| 192 | stopAll: () => Effect.void, | 191 | stopAll: () => Effect.void, |
| ... | @@ -384,43 +383,6 @@ describe("ProviderCommandReactor", () => { | ... | @@ -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 | it("reuses the same provider session when runtime mode is unchanged", async () => { | 386 | it("reuses the same provider session when runtime mode is unchanged", async () => { |
| 425 | const harness = await createHarness(); | 387 | const harness = await createHarness(); |
| 426 | const now = new Date().toISOString(); | 388 | const now = new Date().toISOString(); |
| ... | @@ -467,111 +429,6 @@ describe("ProviderCommandReactor", () => { | ... | @@ -467,111 +429,6 @@ describe("ProviderCommandReactor", () => { |
| 467 | expect(harness.stopSession.mock.calls.length).toBe(0); | 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 | it("restarts the provider session when runtime mode is updated on the thread", async () => { | 432 | it("restarts the provider session when runtime mode is updated on the thread", async () => { |
| 576 | const harness = await createHarness(); | 433 | const harness = await createHarness(); |
| 577 | const now = new Date().toISOString(); | 434 | const now = new Date().toISOString(); |
| ... | @@ -852,7 +709,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -852,7 +709,7 @@ describe("ProviderCommandReactor", () => { |
| 852 | harness.respondToRequest.mockImplementation(() => | 709 | harness.respondToRequest.mockImplementation(() => |
| 853 | Effect.fail( | 710 | Effect.fail( |
| 854 | new ProviderAdapterRequestError({ | 711 | new ProviderAdapterRequestError({ |
| 855 | provider: "cursor", | 712 | provider: "codex", |
| 856 | method: "session/request_permission", | 713 | method: "session/request_permission", |
| 857 | detail: "Unknown pending permission request: approval-request-1", | 714 | detail: "Unknown pending permission request: approval-request-1", |
| 858 | }), | 715 | }), |
| ... | @@ -867,7 +724,7 @@ describe("ProviderCommandReactor", () => { | ... | @@ -867,7 +724,7 @@ describe("ProviderCommandReactor", () => { |
| 867 | session: { | 724 | session: { |
| 868 | threadId: ThreadId.makeUnsafe("thread-1"), | 725 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 869 | status: "running", | 726 | status: "running", |
| 870 | providerName: "cursor", | 727 | providerName: "codex", |
| 871 | runtimeMode: "approval-required", | 728 | runtimeMode: "approval-required", |
| 872 | activeTurnId: null, | 729 | activeTurnId: null, |
| 873 | lastError: null, | 730 | lastError: null, |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+1-4| ... | @@ -211,10 +211,7 @@ const make = Effect.gen(function* () { | ... | @@ -211,10 +211,7 @@ const make = Effect.gen(function* () { |
| 211 | 211 | ||
| 212 | const desiredRuntimeMode = thread.runtimeMode; | 212 | const desiredRuntimeMode = thread.runtimeMode; |
| 213 | const currentProvider: ProviderKind | undefined = | 213 | const currentProvider: ProviderKind | undefined = |
| 214 | thread.session?.providerName === "codex" || | 214 | thread.session?.providerName === "codex" ? thread.session.providerName : undefined; |
| 215 | thread.session?.providerName === "cursor" | ||
| 216 | ? thread.session.providerName | ||
| 217 | : undefined; | ||
| 218 | const preferredProvider: ProviderKind | undefined = options?.provider ?? currentProvider; | 215 | const preferredProvider: ProviderKind | undefined = options?.provider ?? currentProvider; |
| 219 | const desiredModel = options?.model ?? thread.model; | 216 | const desiredModel = options?.model ?? thread.model; |
| 220 | const effectiveCwd = resolveThreadWorkspaceCwd({ | 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,7 +41,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); |
| 41 | type LegacyProviderRuntimeEvent = { | 41 | type LegacyProviderRuntimeEvent = { |
| 42 | readonly type: string; | 42 | readonly type: string; |
| 43 | readonly eventId: EventId; | 43 | readonly eventId: EventId; |
| 44 | readonly provider: "codex" | "cursor"; | 44 | readonly provider: "codex"; |
| 45 | readonly createdAt: string; | 45 | readonly createdAt: string; |
| 46 | readonly threadId: ThreadId; | 46 | readonly threadId: ThreadId; |
| 47 | readonly turnId?: string | undefined; | 47 | readonly turnId?: string | undefined; |
apps/server/src/provider/Layers/CodexAdapter.test.ts-24| ... | @@ -157,30 +157,6 @@ const validationLayer = it.layer( | ... | @@ -157,30 +157,6 @@ const validationLayer = it.layer( |
| 157 | ); | 157 | ); |
| 158 | 158 | ||
| 159 | validationLayer("CodexAdapterLive validation", (it) => { | 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 | it.effect("maps codex model options before starting a session", () => | 160 | it.effect("maps codex model options before starting a session", () => |
| 185 | Effect.gen(function* () { | 161 | Effect.gen(function* () { |
| 186 | validationManager.startSessionImpl.mockClear(); | 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,7 +51,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); |
| 51 | type LegacyProviderRuntimeEvent = { | 51 | type LegacyProviderRuntimeEvent = { |
| 52 | readonly type: string; | 52 | readonly type: string; |
| 53 | readonly eventId: EventId; | 53 | readonly eventId: EventId; |
| 54 | readonly provider: "codex" | "cursor"; | 54 | readonly provider: "codex"; |
| 55 | readonly createdAt: string; | 55 | readonly createdAt: string; |
| 56 | readonly threadId: ThreadId; | 56 | readonly threadId: ThreadId; |
| 57 | readonly turnId?: string | undefined; | 57 | readonly turnId?: string | undefined; |
apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts-22| ... | @@ -174,26 +174,4 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL | ... | @@ -174,26 +174,4 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 174 | 174 | ||
| 175 | fs.rmSync(tempDir, { recursive: true, force: true }); | 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,7 +25,7 @@ function decodeProviderKind( |
| 25 | providerName: string, | 25 | providerName: string, |
| 26 | operation: string, | 26 | operation: string, |
| 27 | ): Effect.Effect<ProviderKind, ProviderSessionDirectoryPersistenceError> { | 27 | ): Effect.Effect<ProviderKind, ProviderSessionDirectoryPersistenceError> { |
| 28 | if (providerName === "codex" || providerName === "cursor") { | 28 | if (providerName === "codex") { |
| 29 | return Effect.succeed(providerName); | 29 | return Effect.succeed(providerName); |
| 30 | } | 30 | } |
| 31 | return Effect.fail( | 31 | return Effect.fail( |
apps/web/src/appSettings.test.ts-21| ... | @@ -15,13 +15,6 @@ describe("normalizeCustomModelSlugs", () => { | ... | @@ -15,13 +15,6 @@ describe("normalizeCustomModelSlugs", () => { |
| 15 | ]), | 15 | ]), |
| 16 | ).toEqual(["custom/internal-model"]); | 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 | describe("getAppModelOptions", () => { | 20 | describe("getAppModelOptions", () => { |
| ... | @@ -47,14 +40,6 @@ describe("getAppModelOptions", () => { | ... | @@ -47,14 +40,6 @@ describe("getAppModelOptions", () => { |
| 47 | isCustom: true, | 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 | describe("getSlashModelOptions", () => { | 45 | describe("getSlashModelOptions", () => { |
| ... | @@ -79,10 +64,4 @@ describe("getSlashModelOptions", () => { | ... | @@ -79,10 +64,4 @@ describe("getSlashModelOptions", () => { |
| 79 | 64 | ||
| 80 | expect(options.map((option) => option.slug)).toEqual(["openai/gpt-oss-120b"]); | 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,7 +8,6 @@ const MAX_CUSTOM_MODEL_COUNT = 32; |
| 8 | export const MAX_CUSTOM_MODEL_LENGTH = 256; | 8 | export const MAX_CUSTOM_MODEL_LENGTH = 256; |
| 9 | const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record<ProviderKind, ReadonlySet<string>> = { | 9 | const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record<ProviderKind, ReadonlySet<string>> = { |
| 10 | codex: new Set(getModelOptions("codex").map((option) => option.slug)), | 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 | const AppSettingsSchema = Schema.Struct({ | 13 | const AppSettingsSchema = Schema.Struct({ |
| ... | @@ -25,9 +24,6 @@ const AppSettingsSchema = Schema.Struct({ | ... | @@ -25,9 +24,6 @@ const AppSettingsSchema = Schema.Struct({ |
| 25 | customCodexModels: Schema.Array(Schema.String).pipe( | 24 | customCodexModels: Schema.Array(Schema.String).pipe( |
| 26 | Schema.withConstructorDefault(() => Option.some([])), | 25 | Schema.withConstructorDefault(() => Option.some([])), |
| 27 | ), | 26 | ), |
| 28 | customCursorModels: Schema.Array(Schema.String).pipe( | ||
| 29 | Schema.withConstructorDefault(() => Option.some([])), | ||
| 30 | ), | ||
| 31 | }); | 27 | }); |
| 32 | export type AppSettings = typeof AppSettingsSchema.Type; | 28 | export type AppSettings = typeof AppSettingsSchema.Type; |
| 33 | export interface AppModelOption { | 29 | export interface AppModelOption { |
| ... | @@ -75,7 +71,6 @@ function normalizeAppSettings(settings: AppSettings): AppSettings { | ... | @@ -75,7 +71,6 @@ function normalizeAppSettings(settings: AppSettings): AppSettings { |
| 75 | return { | 71 | return { |
| 76 | ...settings, | 72 | ...settings, |
| 77 | customCodexModels: normalizeCustomModelSlugs(settings.customCodexModels, "codex"), | 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,12 +1,10 @@ |
| 1 | import { | 1 | import { |
| 2 | type ApprovalRequestId, | 2 | type ApprovalRequestId, |
| 3 | CURSOR_REASONING_OPTIONS, | ||
| 4 | DEFAULT_MODEL, | 3 | DEFAULT_MODEL, |
| 5 | EDITORS, | 4 | EDITORS, |
| 6 | type EditorId, | 5 | type EditorId, |
| 7 | type KeybindingCommand, | 6 | type KeybindingCommand, |
| 8 | type CodexReasoningEffort, | 7 | type CodexReasoningEffort, |
| 9 | type CursorReasoningOption, | ||
| 10 | type MessageId, | 8 | type MessageId, |
| 11 | type ProjectId, | 9 | type ProjectId, |
| 12 | type ProjectEntry, | 10 | type ProjectEntry, |
| ... | @@ -27,12 +25,8 @@ import { | ... | @@ -27,12 +25,8 @@ import { |
| 27 | import { | 25 | import { |
| 28 | getDefaultModel, | 26 | getDefaultModel, |
| 29 | getDefaultReasoningEffort, | 27 | getDefaultReasoningEffort, |
| 30 | getCursorModelCapabilities, | ||
| 31 | getCursorModelFamilyOptions, | ||
| 32 | getReasoningEffortOptions, | 28 | getReasoningEffortOptions, |
| 33 | normalizeModelSlug, | 29 | normalizeModelSlug, |
| 34 | parseCursorModelSelection, | ||
| 35 | resolveCursorModelFromSelection, | ||
| 36 | resolveModelSlugForProvider, | 30 | resolveModelSlugForProvider, |
| 37 | } from "@t3tools/shared/model"; | 31 | } from "@t3tools/shared/model"; |
| 38 | import { | 32 | import { |
| ... | @@ -761,10 +755,6 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -761,10 +755,6 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 761 | ? (sessionProvider ?? selectedProviderByThreadId ?? null) | 755 | ? (sessionProvider ?? selectedProviderByThreadId ?? null) |
| 762 | : null; | 756 | : null; |
| 763 | const selectedProvider: ProviderKind = lockedProvider ?? selectedProviderByThreadId ?? "codex"; | 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 | const baseThreadModel = resolveModelSlugForProvider( | 758 | const baseThreadModel = resolveModelSlugForProvider( |
| 769 | selectedProvider, | 759 | selectedProvider, |
| 770 | activeThread?.model ?? | 760 | activeThread?.model ?? |
| ... | @@ -811,40 +801,16 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -811,40 +801,16 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 811 | }; | 801 | }; |
| 812 | return Object.keys(codexOptions).length > 0 ? { codex: codexOptions } : undefined; | 802 | return Object.keys(codexOptions).length > 0 ? { codex: codexOptions } : undefined; |
| 813 | }, [selectedCodexFastModeEnabled, selectedEffort, selectedProvider, supportsReasoningEffort]); | 803 | }, [selectedCodexFastModeEnabled, selectedEffort, selectedProvider, supportsReasoningEffort]); |
| 814 | const selectedCursorModel = useMemo( | 804 | const selectedModelForPicker = selectedModel; |
| 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; | ||
| 832 | const modelOptionsByProvider = useMemo( | 805 | const modelOptionsByProvider = useMemo( |
| 833 | () => getCustomModelOptionsByProvider(settings), | 806 | () => getCustomModelOptionsByProvider(settings), |
| 834 | [settings], | 807 | [settings], |
| 835 | ); | 808 | ); |
| 836 | const selectedModelForPickerWithCustomFallback = useMemo(() => { | 809 | const selectedModelForPickerWithCustomFallback = useMemo(() => { |
| 837 | if (selectedProvider !== "cursor") { | 810 | const currentOptions = modelOptionsByProvider[selectedProvider]; |
| 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; | ||
| 845 | return currentOptions.some((option) => option.slug === selectedModelForPicker) | 811 | return currentOptions.some((option) => option.slug === selectedModelForPicker) |
| 846 | ? selectedModelForPicker | 812 | ? selectedModelForPicker |
| 847 | : selectedModelForPicker; | 813 | : (normalizeModelSlug(selectedModelForPicker, selectedProvider) ?? selectedModelForPicker); |
| 848 | }, [modelOptionsByProvider, selectedModelForPicker, selectedProvider]); | 814 | }, [modelOptionsByProvider, selectedModelForPicker, selectedProvider]); |
| 849 | const searchableModelOptions = useMemo( | 815 | const searchableModelOptions = useMemo( |
| 850 | () => | 816 | () => |
| ... | @@ -3009,73 +2975,23 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -3009,73 +2975,23 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3009 | const onProviderModelSelect = useCallback( | 2975 | const onProviderModelSelect = useCallback( |
| 3010 | (provider: ProviderKind, model: ModelSlug) => { | 2976 | (provider: ProviderKind, model: ModelSlug) => { |
| 3011 | if (!activeThread) return; | 2977 | if (!activeThread) return; |
| 3012 | if (cursorModelSelectionLockedReason !== null && provider === "cursor") { | ||
| 3013 | scheduleComposerFocus(); | ||
| 3014 | return; | ||
| 3015 | } | ||
| 3016 | if (lockedProvider !== null && provider !== lockedProvider) { | 2978 | if (lockedProvider !== null && provider !== lockedProvider) { |
| 3017 | scheduleComposerFocus(); | 2979 | scheduleComposerFocus(); |
| 3018 | return; | 2980 | return; |
| 3019 | } | 2981 | } |
| 3020 | const resolvedModel = | 2982 | const resolvedModel = resolveModelSlugForProvider(provider, model); |
| 3021 | provider === "cursor" | ||
| 3022 | ? resolveCursorModelFromSelection(parseCursorModelSelection(model)) | ||
| 3023 | : resolveModelSlugForProvider(provider, model); | ||
| 3024 | setComposerDraftProvider(activeThread.id, provider); | 2983 | setComposerDraftProvider(activeThread.id, provider); |
| 3025 | setComposerDraftModel(activeThread.id, resolvedModel); | 2984 | setComposerDraftModel(activeThread.id, resolvedModel); |
| 3026 | scheduleComposerFocus(); | 2985 | scheduleComposerFocus(); |
| 3027 | }, | 2986 | }, |
| 3028 | [ | 2987 | [ |
| 3029 | activeThread, | 2988 | activeThread, |
| 3030 | cursorModelSelectionLockedReason, | ||
| 3031 | lockedProvider, | 2989 | lockedProvider, |
| 3032 | scheduleComposerFocus, | 2990 | scheduleComposerFocus, |
| 3033 | setComposerDraftModel, | 2991 | setComposerDraftModel, |
| 3034 | setComposerDraftProvider, | 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 | const onEffortSelect = useCallback( | 2995 | const onEffortSelect = useCallback( |
| 3080 | (effort: CodexReasoningEffort) => { | 2996 | (effort: CodexReasoningEffort) => { |
| 3081 | setComposerDraftEffort(threadId, effort); | 2997 | setComposerDraftEffort(threadId, effort); |
| ... | @@ -3594,82 +3510,15 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -3594,82 +3510,15 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3594 | <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"> | 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 | <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"> | 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 | {/* Provider/model picker */} | 3512 | {/* Provider/model picker */} |
| 3597 | {cursorModelSelectionLockedReason ? ( | 3513 | <ProviderModelPicker |
| 3598 | <Tooltip> | 3514 | provider={selectedProvider} |
| 3599 | <TooltipTrigger | 3515 | model={selectedModelForPickerWithCustomFallback} |
| 3600 | render={ | 3516 | lockedProvider={lockedProvider} |
| 3601 | <span className="inline-flex"> | 3517 | modelOptionsByProvider={modelOptionsByProvider} |
| 3602 | <ProviderModelPicker | 3518 | onProviderModelChange={onProviderModelSelect} |
| 3603 | provider={selectedProvider} | 3519 | /> |
| 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 | )} | ||
| 3632 | 3520 | ||
| 3633 | {selectedCursorModel && | 3521 | {selectedProvider === "codex" && selectedEffort != null ? ( |
| 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 ? ( | ||
| 3673 | <> | 3522 | <> |
| 3674 | <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" /> | 3523 | <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" /> |
| 3675 | <CodexTraitsPicker | 3524 | <CodexTraitsPicker |
| ... | @@ -5297,18 +5146,9 @@ const COMING_SOON_PROVIDER_OPTIONS = [ | ... | @@ -5297,18 +5146,9 @@ const COMING_SOON_PROVIDER_OPTIONS = [ |
| 5297 | 5146 | ||
| 5298 | function getCustomModelOptionsByProvider(settings: { | 5147 | function getCustomModelOptionsByProvider(settings: { |
| 5299 | customCodexModels: readonly string[]; | 5148 | customCodexModels: readonly string[]; |
| 5300 | customCursorModels: readonly string[]; | ||
| 5301 | }): Record<ProviderKind, ReadonlyArray<{ slug: string; name: string }>> { | 5149 | }): Record<ProviderKind, ReadonlyArray<{ slug: string; name: string }>> { |
| 5302 | const cursorFamilyOptions = getCursorModelFamilyOptions(); | ||
| 5303 | return { | 5150 | return { |
| 5304 | codex: getAppModelOptions("codex", settings.customCodexModels), | 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,10 +5188,6 @@ function resolveModelForProviderPicker( |
| 5348 | return resolved.slug; | 5188 | return resolved.slug; |
| 5349 | } | 5189 | } |
| 5350 | 5190 | ||
| 5351 | if (provider === "cursor") { | ||
| 5352 | return parseCursorModelSelection(normalized).family; | ||
| 5353 | } | ||
| 5354 | |||
| 5355 | return null; | 5191 | return null; |
| 5356 | } | 5192 | } |
| 5357 | 5193 | ||
| ... | @@ -5562,117 +5398,6 @@ const CodexTraitsPicker = memo(function CodexTraitsPicker(props: { | ... | @@ -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 | const OpenInPicker = memo(function OpenInPicker({ | 5401 | const OpenInPicker = memo(function OpenInPicker({ |
| 5677 | keybindings, | 5402 | keybindings, |
| 5678 | availableEditors, | 5403 | availableEditors, |
apps/web/src/composerDraftStore.test.ts+3-3| ... | @@ -394,15 +394,15 @@ describe("composerDraftStore setProvider", () => { | ... | @@ -394,15 +394,15 @@ describe("composerDraftStore setProvider", () => { |
| 394 | it("persists provider-only selection even when prompt/model are empty", () => { | 394 | it("persists provider-only selection even when prompt/model are empty", () => { |
| 395 | const store = useComposerDraftStore.getState(); | 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 | it("removes empty provider-only draft when provider is reset", () => { | 402 | it("removes empty provider-only draft when provider is reset", () => { |
| 403 | const store = useComposerDraftStore.getState(); | 403 | const store = useComposerDraftStore.getState(); |
| 404 | 404 | ||
| 405 | store.setProvider(threadId, "cursor"); | 405 | store.setProvider(threadId, "codex"); |
| 406 | store.setProvider(threadId, null); | 406 | store.setProvider(threadId, null); |
| 407 | 407 | ||
| 408 | expect(useComposerDraftStore.getState().draftsByThreadId[threadId]).toBeUndefined(); | 408 | expect(useComposerDraftStore.getState().draftsByThreadId[threadId]).toBeUndefined(); |
apps/web/src/composerDraftStore.ts+1-1| ... | @@ -208,7 +208,7 @@ function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean { | ... | @@ -208,7 +208,7 @@ function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean { |
| 208 | } | 208 | } |
| 209 | 209 | ||
| 210 | function normalizeProviderKind(value: unknown): ProviderKind | null { | 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 | function revokeObjectPreviewUrl(previewUrl: string): void { | 214 | function revokeObjectPreviewUrl(previewUrl: string): void { |
apps/web/src/routes/_chat.settings.tsx-7| ... | @@ -54,8 +54,6 @@ function getCustomModelsForProvider( | ... | @@ -54,8 +54,6 @@ function getCustomModelsForProvider( |
| 54 | provider: ProviderKind, | 54 | provider: ProviderKind, |
| 55 | ) { | 55 | ) { |
| 56 | switch (provider) { | 56 | switch (provider) { |
| 57 | case "cursor": | ||
| 58 | return settings.customCursorModels; | ||
| 59 | case "codex": | 57 | case "codex": |
| 60 | default: | 58 | default: |
| 61 | return settings.customCodexModels; | 59 | return settings.customCodexModels; |
| ... | @@ -67,8 +65,6 @@ function getDefaultCustomModelsForProvider( | ... | @@ -67,8 +65,6 @@ function getDefaultCustomModelsForProvider( |
| 67 | provider: ProviderKind, | 65 | provider: ProviderKind, |
| 68 | ) { | 66 | ) { |
| 69 | switch (provider) { | 67 | switch (provider) { |
| 70 | case "cursor": | ||
| 71 | return defaults.customCursorModels; | ||
| 72 | case "codex": | 68 | case "codex": |
| 73 | default: | 69 | default: |
| 74 | return defaults.customCodexModels; | 70 | return defaults.customCodexModels; |
| ... | @@ -77,8 +73,6 @@ function getDefaultCustomModelsForProvider( | ... | @@ -77,8 +73,6 @@ function getDefaultCustomModelsForProvider( |
| 77 | 73 | ||
| 78 | function patchCustomModels(provider: ProviderKind, models: string[]) { | 74 | function patchCustomModels(provider: ProviderKind, models: string[]) { |
| 79 | switch (provider) { | 75 | switch (provider) { |
| 80 | case "cursor": | ||
| 81 | return { customCursorModels: models }; | ||
| 82 | case "codex": | 76 | case "codex": |
| 83 | default: | 77 | default: |
| 84 | return { customCodexModels: models }; | 78 | return { customCodexModels: models }; |
| ... | @@ -95,7 +89,6 @@ function SettingsRouteView() { | ... | @@ -95,7 +89,6 @@ function SettingsRouteView() { |
| 95 | Record<ProviderKind, string> | 89 | Record<ProviderKind, string> |
| 96 | >({ | 90 | >({ |
| 97 | codex: "", | 91 | codex: "", |
| 98 | cursor: "", | ||
| 99 | }); | 92 | }); |
| 100 | const [customModelErrorByProvider, setCustomModelErrorByProvider] = useState< | 93 | const [customModelErrorByProvider, setCustomModelErrorByProvider] = useState< |
| 101 | Partial<Record<ProviderKind, string | null>> | 94 | Partial<Record<ProviderKind, string | null>> |
apps/web/src/session-logic.ts+1-1| ... | @@ -10,7 +10,7 @@ import { | ... | @@ -10,7 +10,7 @@ import { |
| 10 | 10 | ||
| 11 | import type { ChatMessage, ProposedPlan, SessionPhase, ThreadSession, TurnDiffSummary } from "./types"; | 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 | export const PROVIDER_OPTIONS: Array<{ | 15 | export const PROVIDER_OPTIONS: Array<{ |
| 16 | value: ProviderPickerKind; | 16 | value: ProviderPickerKind; |
apps/web/src/store.test.ts-23| ... | @@ -141,27 +141,4 @@ describe("store read model sync", () => { | ... | @@ -141,27 +141,4 @@ describe("store read model sync", () => { |
| 141 | 141 | ||
| 142 | expect(next.threads[0]?.model).toBe("gpt-5.3-codex"); | 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,43 +145,25 @@ function toLegacySessionStatus( |
| 145 | } | 145 | } |
| 146 | 146 | ||
| 147 | function toLegacyProvider(providerName: string | null): ProviderKind { | 147 | function toLegacyProvider(providerName: string | null): ProviderKind { |
| 148 | if (providerName === "codex" || providerName === "cursor") { | 148 | if (providerName === "codex") { |
| 149 | return providerName; | 149 | return providerName; |
| 150 | } | 150 | } |
| 151 | return "codex"; | 151 | return "codex"; |
| 152 | } | 152 | } |
| 153 | 153 | ||
| 154 | const CODEX_MODEL_SLUGS = new Set<string>(getModelOptions("codex").map((option) => option.slug)); | 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 | function inferProviderForThreadModel(input: { | 156 | function inferProviderForThreadModel(input: { |
| 161 | readonly model: string; | 157 | readonly model: string; |
| 162 | readonly sessionProviderName: string | null; | 158 | readonly sessionProviderName: string | null; |
| 163 | }): ProviderKind { | 159 | }): ProviderKind { |
| 164 | if ( | 160 | if (input.sessionProviderName === "codex") { |
| 165 | input.sessionProviderName === "codex" || | ||
| 166 | input.sessionProviderName === "cursor" | ||
| 167 | ) { | ||
| 168 | return input.sessionProviderName; | 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 | const normalizedCodex = normalizeModelSlug(input.model, "codex"); | 163 | const normalizedCodex = normalizeModelSlug(input.model, "codex"); |
| 175 | if (normalizedCodex && CODEX_MODEL_SLUGS.has(normalizedCodex)) { | 164 | if (normalizedCodex && CODEX_MODEL_SLUGS.has(normalizedCodex)) { |
| 176 | return "codex"; | 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 | return "codex"; | 167 | return "codex"; |
| 186 | } | 168 | } |
| 187 | 169 |
packages/contracts/src/model.ts+1-70| ... | @@ -1,8 +1,5 @@ | ... | @@ -1,8 +1,5 @@ |
| 1 | import { Schema } from "effect"; | 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 | export const CODEX_REASONING_EFFORT_OPTIONS = ["xhigh", "high", "medium", "low"] as const; | 3 | export const CODEX_REASONING_EFFORT_OPTIONS = ["xhigh", "high", "medium", "low"] as const; |
| 7 | export type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORT_OPTIONS)[number]; | 4 | export type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORT_OPTIONS)[number]; |
| 8 | 5 | ||
| ... | @@ -12,16 +9,8 @@ export const CodexModelOptions = Schema.Struct({ | ... | @@ -12,16 +9,8 @@ export const CodexModelOptions = Schema.Struct({ |
| 12 | }); | 9 | }); |
| 13 | export type CodexModelOptions = typeof CodexModelOptions.Type; | 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 | export const ProviderModelOptions = Schema.Struct({ | 12 | export const ProviderModelOptions = Schema.Struct({ |
| 23 | codex: Schema.optional(CodexModelOptions), | 13 | codex: Schema.optional(CodexModelOptions), |
| 24 | cursor: Schema.optional(CursorModelOptions), | ||
| 25 | }); | 14 | }); |
| 26 | export type ProviderModelOptions = typeof ProviderModelOptions.Type; | 15 | export type ProviderModelOptions = typeof ProviderModelOptions.Type; |
| 27 | 16 | ||
| ... | @@ -30,25 +19,6 @@ type ModelOption = { | ... | @@ -30,25 +19,6 @@ type ModelOption = { |
| 30 | readonly name: string; | 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 | export const MODEL_OPTIONS_BY_PROVIDER = { | 22 | export const MODEL_OPTIONS_BY_PROVIDER = { |
| 53 | codex: [ | 23 | codex: [ |
| 54 | { slug: "gpt-5.4", name: "GPT-5.4" }, | 24 | { slug: "gpt-5.4", name: "GPT-5.4" }, |
| ... | @@ -57,37 +27,14 @@ export const MODEL_OPTIONS_BY_PROVIDER = { | ... | @@ -57,37 +27,14 @@ export const MODEL_OPTIONS_BY_PROVIDER = { |
| 57 | { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" }, | 27 | { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" }, |
| 58 | { slug: "gpt-5.2", name: "GPT-5.2" }, | 28 | { slug: "gpt-5.2", name: "GPT-5.2" }, |
| 59 | ], | 29 | ], |
| 60 | cursor: [ | 30 | } as const satisfies Record<"codex", readonly ModelOption[]>; |
| 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[]>; | ||
| 82 | 31 | ||
| 83 | type BuiltInModelSlug = | 32 | type BuiltInModelSlug = |
| 84 | (typeof MODEL_OPTIONS_BY_PROVIDER)[keyof typeof MODEL_OPTIONS_BY_PROVIDER][number]["slug"]; | 33 | (typeof MODEL_OPTIONS_BY_PROVIDER)[keyof typeof MODEL_OPTIONS_BY_PROVIDER][number]["slug"]; |
| 85 | export type ModelSlug = BuiltInModelSlug | (string & {}); | 34 | export type ModelSlug = BuiltInModelSlug | (string & {}); |
| 86 | export type CursorModelSlug = (typeof MODEL_OPTIONS_BY_PROVIDER)["cursor"][number]["slug"]; | ||
| 87 | 35 | ||
| 88 | export const DEFAULT_MODEL_BY_PROVIDER = { | 36 | export const DEFAULT_MODEL_BY_PROVIDER = { |
| 89 | codex: "gpt-5.3-codex", | 37 | codex: "gpt-5.3-codex", |
| 90 | cursor: "opus-4.6-thinking", | ||
| 91 | } as const satisfies Record<keyof typeof MODEL_OPTIONS_BY_PROVIDER, ModelSlug>; | 38 | } as const satisfies Record<keyof typeof MODEL_OPTIONS_BY_PROVIDER, ModelSlug>; |
| 92 | 39 | ||
| 93 | // Backward compatibility for existing Codex-only call sites. | 40 | // Backward compatibility for existing Codex-only call sites. |
| ... | @@ -102,25 +49,10 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER = { | ... | @@ -102,25 +49,10 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER = { |
| 102 | "5.3-spark": "gpt-5.3-codex-spark", | 49 | "5.3-spark": "gpt-5.3-codex-spark", |
| 103 | "gpt-5.3-spark": "gpt-5.3-codex-spark", | 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 | } as const satisfies Record<keyof typeof MODEL_OPTIONS_BY_PROVIDER, Record<string, ModelSlug>>; | 52 | } as const satisfies Record<keyof typeof MODEL_OPTIONS_BY_PROVIDER, Record<string, ModelSlug>>; |
| 120 | 53 | ||
| 121 | export const REASONING_EFFORT_OPTIONS_BY_PROVIDER = { | 54 | export const REASONING_EFFORT_OPTIONS_BY_PROVIDER = { |
| 122 | codex: CODEX_REASONING_EFFORT_OPTIONS, | 55 | codex: CODEX_REASONING_EFFORT_OPTIONS, |
| 123 | cursor: [], | ||
| 124 | } as const satisfies Record< | 56 | } as const satisfies Record< |
| 125 | keyof typeof MODEL_OPTIONS_BY_PROVIDER, | 57 | keyof typeof MODEL_OPTIONS_BY_PROVIDER, |
| 126 | readonly CodexReasoningEffort[] | 58 | readonly CodexReasoningEffort[] |
| ... | @@ -128,5 +60,4 @@ export const REASONING_EFFORT_OPTIONS_BY_PROVIDER = { | ... | @@ -128,5 +60,4 @@ export const REASONING_EFFORT_OPTIONS_BY_PROVIDER = { |
| 128 | 60 | ||
| 129 | export const DEFAULT_REASONING_EFFORT_BY_PROVIDER = { | 61 | export const DEFAULT_REASONING_EFFORT_BY_PROVIDER = { |
| 130 | codex: "high", | 62 | codex: "high", |
| 131 | cursor: null, | ||
| 132 | } as const satisfies Record<keyof typeof MODEL_OPTIONS_BY_PROVIDER, CodexReasoningEffort | null>; | 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,11 +128,11 @@ it.effect("preserves explicit provider and runtime mode in thread.turn.start", ( |
| 128 | text: "hello", | 128 | text: "hello", |
| 129 | attachments: [], | 129 | attachments: [], |
| 130 | }, | 130 | }, |
| 131 | provider: "cursor", | 131 | provider: "codex", |
| 132 | runtimeMode: "full-access", | 132 | runtimeMode: "full-access", |
| 133 | createdAt: "2026-01-01T00:00:00.000Z", | 133 | createdAt: "2026-01-01T00:00:00.000Z", |
| 134 | }); | 134 | }); |
| 135 | assert.strictEqual(parsed.provider, "cursor"); | 135 | assert.strictEqual(parsed.provider, "codex"); |
| 136 | assert.strictEqual(parsed.runtimeMode, "full-access"); | 136 | assert.strictEqual(parsed.runtimeMode, "full-access"); |
| 137 | assert.strictEqual(parsed.interactionMode, DEFAULT_PROVIDER_INTERACTION_MODE); | 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,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 | it.effect( | 169 | it.effect( |
| 190 | "decodes thread.turn-start-requested defaults for provider, runtime mode, and interaction mode", | 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,7 +27,7 @@ export const ORCHESTRATION_WS_CHANNELS = { |
| 27 | domainEvent: "orchestration.domainEvent", | 27 | domainEvent: "orchestration.domainEvent", |
| 28 | } as const; | 28 | } as const; |
| 29 | 29 | ||
| 30 | export const ProviderKind = Schema.Literals(["codex", "cursor"]); | 30 | export const ProviderKind = Schema.Literal("codex"); |
| 31 | export type ProviderKind = typeof ProviderKind.Type; | 31 | export type ProviderKind = typeof ProviderKind.Type; |
| 32 | export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex"; | 32 | export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex"; |
| 33 | export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); | 33 | export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); |
packages/contracts/src/provider.test.ts-25| ... | @@ -42,31 +42,6 @@ describe("ProviderSessionStartInput", () => { | ... | @@ -42,31 +42,6 @@ describe("ProviderSessionStartInput", () => { |
| 42 | }), | 42 | }), |
| 43 | ).toThrow(); | 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 | describe("ProviderSendTurnInput", () => { | 47 | describe("ProviderSendTurnInput", () => { |
packages/contracts/src/provider.ts-5| ... | @@ -50,13 +50,8 @@ const CodexProviderStartOptions = Schema.Struct({ | ... | @@ -50,13 +50,8 @@ const CodexProviderStartOptions = Schema.Struct({ |
| 50 | homePath: Schema.optional(TrimmedNonEmptyStringSchema), | 50 | homePath: Schema.optional(TrimmedNonEmptyStringSchema), |
| 51 | }); | 51 | }); |
| 52 | 52 | ||
| 53 | const CursorProviderStartOptions = Schema.Struct({ | ||
| 54 | binaryPath: Schema.optional(TrimmedNonEmptyStringSchema), | ||
| 55 | }); | ||
| 56 | |||
| 57 | const ProviderStartOptions = Schema.Struct({ | 53 | const ProviderStartOptions = Schema.Struct({ |
| 58 | codex: Schema.optional(CodexProviderStartOptions), | 54 | codex: Schema.optional(CodexProviderStartOptions), |
| 59 | cursor: Schema.optional(CursorProviderStartOptions), | ||
| 60 | }); | 55 | }); |
| 61 | 56 | ||
| 62 | export const ProviderSessionStartInput = Schema.Struct({ | 57 | export const ProviderSessionStartInput = Schema.Struct({ |
packages/contracts/src/providerRuntime.ts-3| ... | @@ -20,9 +20,6 @@ const RuntimeEventRawSource = Schema.Literals([ | ... | @@ -20,9 +20,6 @@ const RuntimeEventRawSource = Schema.Literals([ |
| 20 | "codex.app-server.request", | 20 | "codex.app-server.request", |
| 21 | "codex.eventmsg", | 21 | "codex.eventmsg", |
| 22 | "codex.sdk.thread-event", | 22 | "codex.sdk.thread-event", |
| 23 | "cursor.acp.notification", | ||
| 24 | "cursor.acp.request", | ||
| 25 | "cursor.acp.response", | ||
| 26 | ]); | 23 | ]); |
| 27 | export type RuntimeEventRawSource = typeof RuntimeEventRawSource.Type; | 24 | export type RuntimeEventRawSource = typeof RuntimeEventRawSource.Type; |
| 28 | 25 |
packages/shared/src/model.test.ts+1-89| ... | @@ -1,24 +1,13 @@ | ... | @@ -1,24 +1,13 @@ |
| 1 | import { describe, expect, it } from "vitest"; | 1 | import { describe, expect, it } from "vitest"; |
| 2 | import { | 2 | import { DEFAULT_MODEL, MODEL_OPTIONS } from "@t3tools/contracts"; |
| 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"; | ||
| 10 | 3 | ||
| 11 | import { | 4 | import { |
| 12 | getDefaultModel, | 5 | getDefaultModel, |
| 13 | getDefaultReasoningEffort, | 6 | getDefaultReasoningEffort, |
| 14 | getCursorModelFamilyOptions, | ||
| 15 | getModelOptions, | 7 | getModelOptions, |
| 16 | getReasoningEffortOptions, | 8 | getReasoningEffortOptions, |
| 17 | normalizeModelSlug, | 9 | normalizeModelSlug, |
| 18 | parseCursorModelSelection, | ||
| 19 | resolveCursorModelFromSelection, | ||
| 20 | resolveModelSlug, | 10 | resolveModelSlug, |
| 21 | resolveModelSlugForProvider, | ||
| 22 | } from "./model"; | 11 | } from "./model"; |
| 23 | 12 | ||
| 24 | describe("normalizeModelSlug", () => { | 13 | describe("normalizeModelSlug", () => { |
| ... | @@ -38,17 +27,6 @@ describe("normalizeModelSlug", () => { | ... | @@ -38,17 +27,6 @@ describe("normalizeModelSlug", () => { |
| 38 | expect(normalizeModelSlug("gpt-5.2")).toBe("gpt-5.2"); | 27 | expect(normalizeModelSlug("gpt-5.2")).toBe("gpt-5.2"); |
| 39 | expect(normalizeModelSlug("gpt-5.2-codex")).toBe("gpt-5.2-codex"); | 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 | describe("resolveModelSlug", () => { | 32 | describe("resolveModelSlug", () => { |
| ... | @@ -67,70 +45,9 @@ describe("resolveModelSlug", () => { | ... | @@ -67,70 +45,9 @@ describe("resolveModelSlug", () => { |
| 67 | expect(resolveModelSlug(model.slug)).toBe(model.slug); | 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 | it("keeps codex defaults for backward compatibility", () => { | 48 | it("keeps codex defaults for backward compatibility", () => { |
| 85 | expect(getDefaultModel()).toBe(DEFAULT_MODEL); | 49 | expect(getDefaultModel()).toBe(DEFAULT_MODEL); |
| 86 | expect(getModelOptions()).toEqual(MODEL_OPTIONS); | 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,15 +55,10 @@ describe("getReasoningEffortOptions", () => { |
| 138 | it("returns codex reasoning options for codex", () => { | 55 | it("returns codex reasoning options for codex", () => { |
| 139 | expect(getReasoningEffortOptions("codex")).toEqual(["xhigh", "high", "medium", "low"]); | 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 | describe("getDefaultReasoningEffort", () => { | 60 | describe("getDefaultReasoningEffort", () => { |
| 148 | it("returns provider-scoped defaults", () => { | 61 | it("returns provider-scoped defaults", () => { |
| 149 | expect(getDefaultReasoningEffort("codex")).toBe("high"); | 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,233 +1,26 @@ |
| 1 | import { | 1 | import { |
| 2 | CODEX_REASONING_EFFORT_OPTIONS, | 2 | CODEX_REASONING_EFFORT_OPTIONS, |
| 3 | CURSOR_MODEL_FAMILY_OPTIONS, | ||
| 4 | CURSOR_REASONING_OPTIONS, | ||
| 5 | DEFAULT_MODEL, | 3 | DEFAULT_MODEL, |
| 6 | DEFAULT_MODEL_BY_PROVIDER, | 4 | DEFAULT_MODEL_BY_PROVIDER, |
| 7 | MODEL_OPTIONS_BY_PROVIDER, | 5 | MODEL_OPTIONS_BY_PROVIDER, |
| 8 | MODEL_SLUG_ALIASES_BY_PROVIDER, | 6 | MODEL_SLUG_ALIASES_BY_PROVIDER, |
| 9 | type CodexReasoningEffort, | 7 | type CodexReasoningEffort, |
| 10 | type CursorModelFamily, | ||
| 11 | type CursorModelSlug, | ||
| 12 | type CursorReasoningOption, | ||
| 13 | type ModelSlug, | 8 | type ModelSlug, |
| 14 | type ProviderKind, | 9 | type ProviderKind, |
| 15 | } from "../../contracts/src"; | 10 | } from "@t3tools/contracts"; |
| 16 | 11 | ||
| 17 | type CatalogProvider = keyof typeof MODEL_OPTIONS_BY_PROVIDER; | 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 | const MODEL_SLUG_SET_BY_PROVIDER: Record<CatalogProvider, ReadonlySet<ModelSlug>> = { | 14 | const MODEL_SLUG_SET_BY_PROVIDER: Record<CatalogProvider, ReadonlySet<ModelSlug>> = { |
| 98 | codex: new Set(MODEL_OPTIONS_BY_PROVIDER.codex.map((option) => option.slug)), | 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 | export function getModelOptions(provider: ProviderKind = "codex") { | 18 | export function getModelOptions(provider: ProviderKind = "codex") { |
| 114 | return hasModelCatalog(provider) ? MODEL_OPTIONS_BY_PROVIDER[provider] : []; | 19 | return 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; | ||
| 227 | } | 20 | } |
| 228 | 21 | ||
| 229 | export function getDefaultModel(provider: ProviderKind = "codex"): ModelSlug { | 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 | export function normalizeModelSlug( | 26 | export function normalizeModelSlug( |
| ... | @@ -243,10 +36,6 @@ export function normalizeModelSlug( | ... | @@ -243,10 +36,6 @@ export function normalizeModelSlug( |
| 243 | return null; | 36 | return null; |
| 244 | } | 37 | } |
| 245 | 38 | ||
| 246 | if (!hasModelCatalog(provider)) { | ||
| 247 | return trimmed as ModelSlug; | ||
| 248 | } | ||
| 249 | |||
| 250 | const aliases = MODEL_SLUG_ALIASES_BY_PROVIDER[provider] as Record<string, ModelSlug>; | 39 | const aliases = MODEL_SLUG_ALIASES_BY_PROVIDER[provider] as Record<string, ModelSlug>; |
| 251 | return aliases[trimmed] ?? (trimmed as ModelSlug); | 40 | return aliases[trimmed] ?? (trimmed as ModelSlug); |
| 252 | } | 41 | } |
| ... | @@ -260,11 +49,9 @@ export function resolveModelSlug( | ... | @@ -260,11 +49,9 @@ export function resolveModelSlug( |
| 260 | return getDefaultModel(provider); | 49 | return getDefaultModel(provider); |
| 261 | } | 50 | } |
| 262 | 51 | ||
| 263 | if (!hasModelCatalog(provider)) { | 52 | return MODEL_SLUG_SET_BY_PROVIDER[provider].has(normalized) |
| 264 | return getDefaultModel(provider); | 53 | ? normalized |
| 265 | } | 54 | : getDefaultModel(provider); |
| 266 | |||
| 267 | return MODEL_SLUG_SET_BY_PROVIDER[provider].has(normalized) ? normalized : getDefaultModel(provider); | ||
| 268 | } | 55 | } |
| 269 | 56 | ||
| 270 | export function resolveModelSlugForProvider( | 57 | export function resolveModelSlugForProvider( |