| author | |
| committer | |
| log | 40fcb6bdf8d726f35c6d3aec82d0cce6345c9dcd |
| tree | 7a92e1d9f889c6beb5b0710074fb991995deaafa |
| parent | d58ff5de26b9753c3dbd13cb55e532ccd44ba0d2 |
| signature | Signed by PGP key B5690EEEBB952194 |
* fix: restrict spark to non-plus users & fast mode gpt5.4
* nit
* tests
* tests
---------
Co-authored-by: Theo Browne <me@t3.gg>11 files changed, 400 insertions(+), 22 deletions(-)
apps/server/src/codexAppServerManager.test.ts+111-5| ... | ... | @@ -13,6 +13,8 @@ import { |
| 13 | 13 | classifyCodexStderrLine, |
| 14 | 14 | isRecoverableThreadResumeError, |
| 15 | 15 | normalizeCodexModelSlug, |
| 16 | readCodexAccountSnapshot, | |
| 17 | resolveCodexModelForAccount, | |
| 16 | 18 | } from "./codexAppServerManager"; |
| 17 | 19 | |
| 18 | 20 | const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value); |
| ... | ... | @@ -21,14 +23,20 @@ function createSendTurnHarness() { |
| 21 | 23 | const manager = new CodexAppServerManager(); |
| 22 | 24 | const context = { |
| 23 | 25 | session: { |
| 24 | sessionId: "sess_1", | |
| 25 | 26 | provider: "codex", |
| 26 | 27 | status: "ready", |
| 27 | 28 | threadId: "thread_1", |
| 29 | runtimeMode: "full-access", | |
| 30 | model: "gpt-5.3-codex", | |
| 28 | 31 | resumeCursor: { threadId: "thread_1" }, |
| 29 | 32 | createdAt: "2026-02-10T00:00:00.000Z", |
| 30 | 33 | updatedAt: "2026-02-10T00:00:00.000Z", |
| 31 | 34 | }, |
| 35 | account: { | |
| 36 | type: "unknown", | |
| 37 | planType: null, | |
| 38 | sparkEnabled: true, | |
| 39 | }, | |
| 32 | 40 | }; |
| 33 | 41 | |
| 34 | 42 | const requireSession = vi |
| ... | ... | @@ -58,10 +66,11 @@ function createThreadControlHarness() { |
| 58 | 66 | const manager = new CodexAppServerManager(); |
| 59 | 67 | const context = { |
| 60 | 68 | session: { |
| 61 | sessionId: "sess_1", | |
| 62 | 69 | provider: "codex", |
| 63 | 70 | status: "ready", |
| 64 | 71 | threadId: "thread_1", |
| 72 | runtimeMode: "full-access", | |
| 73 | model: "gpt-5.3-codex", | |
| 65 | 74 | resumeCursor: { threadId: "thread_1" }, |
| 66 | 75 | createdAt: "2026-02-10T00:00:00.000Z", |
| 67 | 76 | updatedAt: "2026-02-10T00:00:00.000Z", |
| ... | ... | @@ -89,10 +98,11 @@ function createPendingUserInputHarness() { |
| 89 | 98 | const manager = new CodexAppServerManager(); |
| 90 | 99 | const context = { |
| 91 | 100 | session: { |
| 92 | sessionId: "sess_1", | |
| 93 | 101 | provider: "codex", |
| 94 | 102 | status: "ready", |
| 95 | 103 | threadId: "thread_1", |
| 104 | runtimeMode: "full-access", | |
| 105 | model: "gpt-5.3-codex", | |
| 96 | 106 | resumeCursor: { threadId: "thread_1" }, |
| 97 | 107 | createdAt: "2026-02-10T00:00:00.000Z", |
| 98 | 108 | updatedAt: "2026-02-10T00:00:00.000Z", |
| ... | ... | @@ -195,6 +205,70 @@ describe("isRecoverableThreadResumeError", () => { |
| 195 | 205 | }); |
| 196 | 206 | }); |
| 197 | 207 | |
| 208 | describe("readCodexAccountSnapshot", () => { | |
| 209 | it("disables spark for chatgpt plus accounts", () => { | |
| 210 | expect( | |
| 211 | readCodexAccountSnapshot({ | |
| 212 | type: "chatgpt", | |
| 213 | email: "plus@example.com", | |
| 214 | planType: "plus", | |
| 215 | }), | |
| 216 | ).toEqual({ | |
| 217 | type: "chatgpt", | |
| 218 | planType: "plus", | |
| 219 | sparkEnabled: false, | |
| 220 | }); | |
| 221 | }); | |
| 222 | ||
| 223 | it("keeps spark enabled for chatgpt pro accounts", () => { | |
| 224 | expect( | |
| 225 | readCodexAccountSnapshot({ | |
| 226 | type: "chatgpt", | |
| 227 | email: "pro@example.com", | |
| 228 | planType: "pro", | |
| 229 | }), | |
| 230 | ).toEqual({ | |
| 231 | type: "chatgpt", | |
| 232 | planType: "pro", | |
| 233 | sparkEnabled: true, | |
| 234 | }); | |
| 235 | }); | |
| 236 | ||
| 237 | it("keeps spark enabled for api key accounts", () => { | |
| 238 | expect( | |
| 239 | readCodexAccountSnapshot({ | |
| 240 | type: "apiKey", | |
| 241 | }), | |
| 242 | ).toEqual({ | |
| 243 | type: "apiKey", | |
| 244 | planType: null, | |
| 245 | sparkEnabled: true, | |
| 246 | }); | |
| 247 | }); | |
| 248 | }); | |
| 249 | ||
| 250 | describe("resolveCodexModelForAccount", () => { | |
| 251 | it("falls back from spark to default for unsupported chatgpt plans", () => { | |
| 252 | expect( | |
| 253 | resolveCodexModelForAccount("gpt-5.3-codex-spark", { | |
| 254 | type: "chatgpt", | |
| 255 | planType: "plus", | |
| 256 | sparkEnabled: false, | |
| 257 | }), | |
| 258 | ).toBe("gpt-5.3-codex"); | |
| 259 | }); | |
| 260 | ||
| 261 | it("keeps spark for supported plans", () => { | |
| 262 | expect( | |
| 263 | resolveCodexModelForAccount("gpt-5.3-codex-spark", { | |
| 264 | type: "chatgpt", | |
| 265 | planType: "pro", | |
| 266 | sparkEnabled: true, | |
| 267 | }), | |
| 268 | ).toBe("gpt-5.3-codex-spark"); | |
| 269 | }); | |
| 270 | }); | |
| 271 | ||
| 198 | 272 | describe("startSession", () => { |
| 199 | 273 | it("enables Codex experimental api capabilities during initialize", () => { |
| 200 | 274 | expect(buildCodexInitializeParams()).toEqual({ |
| ... | ... | @@ -259,8 +333,8 @@ describe("sendTurn", () => { |
| 259 | 333 | }, |
| 260 | 334 | ], |
| 261 | 335 | model: "gpt-5.3", |
| 262 | effort: "high", | |
| 263 | 336 | serviceTier: "fast", |
| 337 | effort: "high", | |
| 264 | 338 | }); |
| 265 | 339 | |
| 266 | 340 | expect(result).toEqual({ |
| ... | ... | @@ -283,8 +357,8 @@ describe("sendTurn", () => { |
| 283 | 357 | }, |
| 284 | 358 | ], |
| 285 | 359 | model: "gpt-5.3-codex", |
| 286 | effort: "high", | |
| 287 | 360 | serviceTier: "fast", |
| 361 | effort: "high", | |
| 288 | 362 | }); |
| 289 | 363 | expect(updateSession).toHaveBeenCalledWith(context, { |
| 290 | 364 | status: "running", |
| ... | ... | @@ -314,6 +388,7 @@ describe("sendTurn", () => { |
| 314 | 388 | url: "data:image/png;base64,BBBB", |
| 315 | 389 | }, |
| 316 | 390 | ], |
| 391 | model: "gpt-5.3-codex", | |
| 317 | 392 | }); |
| 318 | 393 | }); |
| 319 | 394 | |
| ... | ... | @@ -377,6 +452,37 @@ describe("sendTurn", () => { |
| 377 | 452 | }); |
| 378 | 453 | }); |
| 379 | 454 | |
| 455 | it("keeps the session model when interaction mode is set without an explicit model", async () => { | |
| 456 | const { manager, context, sendRequest } = createSendTurnHarness(); | |
| 457 | context.session.model = "gpt-5.2-codex"; | |
| 458 | ||
| 459 | await manager.sendTurn({ | |
| 460 | threadId: asThreadId("thread_1"), | |
| 461 | input: "Plan this with my current session model", | |
| 462 | interactionMode: "plan", | |
| 463 | }); | |
| 464 | ||
| 465 | expect(sendRequest).toHaveBeenCalledWith(context, "turn/start", { | |
| 466 | threadId: "thread_1", | |
| 467 | input: [ | |
| 468 | { | |
| 469 | type: "text", | |
| 470 | text: "Plan this with my current session model", | |
| 471 | text_elements: [], | |
| 472 | }, | |
| 473 | ], | |
| 474 | model: "gpt-5.2-codex", | |
| 475 | collaborationMode: { | |
| 476 | mode: "plan", | |
| 477 | settings: { | |
| 478 | model: "gpt-5.2-codex", | |
| 479 | reasoning_effort: "medium", | |
| 480 | developer_instructions: CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS, | |
| 481 | }, | |
| 482 | }, | |
| 483 | }); | |
| 484 | }); | |
| 485 | ||
| 380 | 486 | it("rejects empty turn input", async () => { |
| 381 | 487 | const { manager } = createSendTurnHarness(); |
| 382 | 488 |
apps/server/src/codexAppServerManager.ts+110-8| ... | ... | @@ -58,6 +58,7 @@ interface CodexUserInputAnswer { |
| 58 | 58 | |
| 59 | 59 | interface CodexSessionContext { |
| 60 | 60 | session: ProviderSession; |
| 61 | account: CodexAccountSnapshot; | |
| 61 | 62 | child: ChildProcessWithoutNullStreams; |
| 62 | 63 | output: readline.Interface; |
| 63 | 64 | pending: Map<PendingRequestKey, PendingRequest>; |
| ... | ... | @@ -89,13 +90,30 @@ interface JsonRpcNotification { |
| 89 | 90 | params?: unknown; |
| 90 | 91 | } |
| 91 | 92 | |
| 93 | type CodexPlanType = | |
| 94 | | "free" | |
| 95 | | "go" | |
| 96 | | "plus" | |
| 97 | | "pro" | |
| 98 | | "team" | |
| 99 | | "business" | |
| 100 | | "enterprise" | |
| 101 | | "edu" | |
| 102 | | "unknown"; | |
| 103 | ||
| 104 | interface CodexAccountSnapshot { | |
| 105 | readonly type: "apiKey" | "chatgpt" | "unknown"; | |
| 106 | readonly planType: CodexPlanType | null; | |
| 107 | readonly sparkEnabled: boolean; | |
| 108 | } | |
| 109 | ||
| 92 | 110 | export interface CodexAppServerSendTurnInput { |
| 93 | 111 | readonly threadId: ThreadId; |
| 94 | 112 | readonly input?: string; |
| 95 | 113 | readonly attachments?: ReadonlyArray<{ type: "image"; url: string }>; |
| 96 | 114 | readonly model?: string; |
| 115 | readonly serviceTier?: string | null; | |
| 97 | 116 | readonly effort?: string; |
| 98 | readonly serviceTier?: string; | |
| 99 | 117 | readonly interactionMode?: ProviderInteractionMode; |
| 100 | 118 | } |
| 101 | 119 | |
| ... | ... | @@ -135,6 +153,50 @@ const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [ |
| 135 | 153 | "unknown thread", |
| 136 | 154 | "does not exist", |
| 137 | 155 | ]; |
| 156 | const CODEX_DEFAULT_MODEL = "gpt-5.3-codex"; | |
| 157 | const CODEX_SPARK_MODEL = "gpt-5.3-codex-spark"; | |
| 158 | const CODEX_SPARK_DISABLED_PLAN_TYPES = new Set<CodexPlanType>(["free", "go", "plus"]); | |
| 159 | ||
| 160 | function asObject(value: unknown): Record<string, unknown> | undefined { | |
| 161 | if (!value || typeof value !== "object") { | |
| 162 | return undefined; | |
| 163 | } | |
| 164 | return value as Record<string, unknown>; | |
| 165 | } | |
| 166 | ||
| 167 | function asString(value: unknown): string | undefined { | |
| 168 | return typeof value === "string" ? value : undefined; | |
| 169 | } | |
| 170 | ||
| 171 | export function readCodexAccountSnapshot(response: unknown): CodexAccountSnapshot { | |
| 172 | const record = asObject(response); | |
| 173 | const account = asObject(record?.account) ?? record; | |
| 174 | const accountType = asString(account?.type); | |
| 175 | ||
| 176 | if (accountType === "apiKey") { | |
| 177 | return { | |
| 178 | type: "apiKey", | |
| 179 | planType: null, | |
| 180 | sparkEnabled: true, | |
| 181 | }; | |
| 182 | } | |
| 183 | ||
| 184 | if (accountType === "chatgpt") { | |
| 185 | const planType = (account?.planType as CodexPlanType | null) ?? "unknown"; | |
| 186 | return { | |
| 187 | type: "chatgpt", | |
| 188 | planType, | |
| 189 | sparkEnabled: !CODEX_SPARK_DISABLED_PLAN_TYPES.has(planType), | |
| 190 | }; | |
| 191 | } | |
| 192 | ||
| 193 | return { | |
| 194 | type: "unknown", | |
| 195 | planType: null, | |
| 196 | sparkEnabled: true, | |
| 197 | }; | |
| 198 | } | |
| 199 | ||
| 138 | 200 | export const CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS = `<collaboration_mode># Plan Mode (Conversational) |
| 139 | 201 | |
| 140 | 202 | You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed-intent- and implementation-wise-so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions. |
| ... | ... | @@ -287,6 +349,17 @@ function mapCodexRuntimeMode(runtimeMode: RuntimeMode): { |
| 287 | 349 | }; |
| 288 | 350 | } |
| 289 | 351 | |
| 352 | export function resolveCodexModelForAccount( | |
| 353 | model: string | undefined, | |
| 354 | account: CodexAccountSnapshot, | |
| 355 | ): string | undefined { | |
| 356 | if (model !== CODEX_SPARK_MODEL || account.sparkEnabled) { | |
| 357 | return model; | |
| 358 | } | |
| 359 | ||
| 360 | return CODEX_DEFAULT_MODEL; | |
| 361 | } | |
| 362 | ||
| 290 | 363 | /** |
| 291 | 364 | * On Windows with `shell: true`, `child.kill()` only terminates the `cmd.exe` |
| 292 | 365 | * wrapper, leaving the actual command running. Use `taskkill /T` to kill the |
| ... | ... | @@ -475,6 +548,11 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 475 | 548 | |
| 476 | 549 | context = { |
| 477 | 550 | session, |
| 551 | account: { | |
| 552 | type: "unknown", | |
| 553 | planType: null, | |
| 554 | sparkEnabled: true, | |
| 555 | }, | |
| 478 | 556 | child, |
| 479 | 557 | output, |
| 480 | 558 | pending: new Map(), |
| ... | ... | @@ -492,12 +570,33 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 492 | 570 | await this.sendRequest(context, "initialize", buildCodexInitializeParams()); |
| 493 | 571 | |
| 494 | 572 | this.writeMessage(context, { method: "initialized" }); |
| 573 | try { | |
| 574 | const modelListResponse = await this.sendRequest(context, "model/list", {}); | |
| 575 | console.log("codex model/list response", modelListResponse); | |
| 576 | } catch (error) { | |
| 577 | console.log("codex model/list failed", error); | |
| 578 | } | |
| 579 | try { | |
| 580 | const accountReadResponse = await this.sendRequest(context, "account/read", {}); | |
| 581 | console.log("codex account/read response", accountReadResponse); | |
| 582 | context.account = readCodexAccountSnapshot(accountReadResponse); | |
| 583 | console.log("codex subscription status", { | |
| 584 | type: context.account.type, | |
| 585 | planType: context.account.planType, | |
| 586 | sparkEnabled: context.account.sparkEnabled, | |
| 587 | }); | |
| 588 | } catch (error) { | |
| 589 | console.log("codex account/read failed", error); | |
| 590 | } | |
| 495 | 591 | |
| 496 | const normalizedModel = normalizeCodexModelSlug(input.model); | |
| 592 | const normalizedModel = resolveCodexModelForAccount( | |
| 593 | normalizeCodexModelSlug(input.model), | |
| 594 | context.account, | |
| 595 | ); | |
| 497 | 596 | const sessionOverrides = { |
| 498 | 597 | model: normalizedModel ?? null, |
| 499 | cwd: input.cwd ?? null, | |
| 500 | 598 | ...(input.serviceTier !== undefined ? { serviceTier: input.serviceTier } : {}), |
| 599 | cwd: input.cwd ?? null, | |
| 501 | 600 | ...mapCodexRuntimeMode(input.runtimeMode ?? "full-access"), |
| 502 | 601 | }; |
| 503 | 602 | |
| ... | ... | @@ -657,8 +756,8 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 657 | 756 | { type: "text"; text: string; text_elements: [] } | { type: "image"; url: string } |
| 658 | 757 | >; |
| 659 | 758 | model?: string; |
| 759 | serviceTier?: string | null; | |
| 660 | 760 | effort?: string; |
| 661 | serviceTier?: string; | |
| 662 | 761 | collaborationMode?: { |
| 663 | 762 | mode: "default" | "plan"; |
| 664 | 763 | settings: { |
| ... | ... | @@ -671,16 +770,19 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 671 | 770 | threadId: providerThreadId, |
| 672 | 771 | input: turnInput, |
| 673 | 772 | }; |
| 674 | const normalizedModel = normalizeCodexModelSlug(input.model ?? context.session.model); | |
| 773 | const normalizedModel = resolveCodexModelForAccount( | |
| 774 | normalizeCodexModelSlug(input.model ?? context.session.model), | |
| 775 | context.account, | |
| 776 | ); | |
| 675 | 777 | if (normalizedModel) { |
| 676 | 778 | turnStartParams.model = normalizedModel; |
| 677 | 779 | } |
| 780 | if (input.serviceTier !== undefined) { | |
| 781 | turnStartParams.serviceTier = input.serviceTier; | |
| 782 | } | |
| 678 | 783 | if (input.effort) { |
| 679 | 784 | turnStartParams.effort = input.effort; |
| 680 | 785 | } |
| 681 | if (input.serviceTier) { | |
| 682 | turnStartParams.serviceTier = input.serviceTier; | |
| 683 | } | |
| 684 | 786 | const collaborationMode = buildCodexCollaborationMode({ |
| 685 | 787 | ...(input.interactionMode !== undefined ? { interactionMode: input.interactionMode } : {}), |
| 686 | 788 | ...(normalizedModel !== undefined ? { model: normalizedModel } : {}), |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+7| ... | ... | @@ -5,6 +5,7 @@ import { |
| 5 | 5 | type OrchestrationEvent, |
| 6 | 6 | type ProviderModelOptions, |
| 7 | 7 | type ProviderKind, |
| 8 | type ProviderServiceTier, | |
| 8 | 9 | type OrchestrationSession, |
| 9 | 10 | ThreadId, |
| 10 | 11 | type ProviderSession, |
| ... | ... | @@ -201,6 +202,7 @@ const make = Effect.gen(function* () { |
| 201 | 202 | readonly provider?: ProviderKind; |
| 202 | 203 | readonly model?: string; |
| 203 | 204 | readonly modelOptions?: ProviderModelOptions; |
| 205 | readonly serviceTier?: ProviderServiceTier | null; | |
| 204 | 206 | }, |
| 205 | 207 | ) { |
| 206 | 208 | const readModel = yield* orchestrationEngine.getReadModel(); |
| ... | ... | @@ -235,6 +237,7 @@ const make = Effect.gen(function* () { |
| 235 | 237 | : {}), |
| 236 | 238 | ...(effectiveCwd ? { cwd: effectiveCwd } : {}), |
| 237 | 239 | ...(desiredModel ? { model: desiredModel } : {}), |
| 240 | ...(options?.serviceTier !== undefined ? { serviceTier: options.serviceTier } : {}), | |
| 238 | 241 | ...(options?.modelOptions !== undefined ? { modelOptions: options.modelOptions } : {}), |
| 239 | 242 | ...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}), |
| 240 | 243 | runtimeMode: desiredRuntimeMode, |
| ... | ... | @@ -320,6 +323,7 @@ const make = Effect.gen(function* () { |
| 320 | 323 | readonly attachments?: ReadonlyArray<ChatAttachment>; |
| 321 | 324 | readonly provider?: ProviderKind; |
| 322 | 325 | readonly model?: string; |
| 326 | readonly serviceTier?: ProviderServiceTier | null; | |
| 323 | 327 | readonly modelOptions?: ProviderModelOptions; |
| 324 | 328 | readonly interactionMode?: "default" | "plan"; |
| 325 | 329 | readonly createdAt: string; |
| ... | ... | @@ -331,6 +335,7 @@ const make = Effect.gen(function* () { |
| 331 | 335 | yield* ensureSessionForThread(input.threadId, input.createdAt, { |
| 332 | 336 | ...(input.provider !== undefined ? { provider: input.provider } : {}), |
| 333 | 337 | ...(input.model !== undefined ? { model: input.model } : {}), |
| 338 | ...(input.serviceTier !== undefined ? { serviceTier: input.serviceTier } : {}), | |
| 334 | 339 | ...(input.modelOptions !== undefined ? { modelOptions: input.modelOptions } : {}), |
| 335 | 340 | }); |
| 336 | 341 | const normalizedInput = toNonEmptyProviderInput(input.messageText); |
| ... | ... | @@ -350,6 +355,7 @@ const make = Effect.gen(function* () { |
| 350 | 355 | ...(normalizedInput ? { input: normalizedInput } : {}), |
| 351 | 356 | ...(normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {}), |
| 352 | 357 | ...(modelForTurn !== undefined ? { model: modelForTurn } : {}), |
| 358 | ...(input.serviceTier !== undefined ? { serviceTier: input.serviceTier } : {}), | |
| 353 | 359 | ...(input.modelOptions !== undefined ? { modelOptions: input.modelOptions } : {}), |
| 354 | 360 | ...(input.interactionMode !== undefined ? { interactionMode: input.interactionMode } : {}), |
| 355 | 361 | }); |
| ... | ... | @@ -464,6 +470,7 @@ const make = Effect.gen(function* () { |
| 464 | 470 | ...(message.attachments !== undefined ? { attachments: message.attachments } : {}), |
| 465 | 471 | ...(event.payload.provider !== undefined ? { provider: event.payload.provider } : {}), |
| 466 | 472 | ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), |
| 473 | ...(event.payload.serviceTier !== undefined ? { serviceTier: event.payload.serviceTier } : {}), | |
| 467 | 474 | ...(event.payload.modelOptions !== undefined ? { modelOptions: event.payload.modelOptions } : {}), |
| 468 | 475 | interactionMode: event.payload.interactionMode, |
| 469 | 476 | createdAt: event.payload.createdAt, |
apps/server/src/orchestration/decider.ts+1| ... | ... | @@ -301,6 +301,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 301 | 301 | messageId: command.message.messageId, |
| 302 | 302 | ...(command.provider !== undefined ? { provider: command.provider } : {}), |
| 303 | 303 | ...(command.model !== undefined ? { model: command.model } : {}), |
| 304 | ...(command.serviceTier !== undefined ? { serviceTier: command.serviceTier } : {}), | |
| 304 | 305 | ...(command.modelOptions !== undefined ? { modelOptions: command.modelOptions } : {}), |
| 305 | 306 | assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE, |
| 306 | 307 | runtimeMode: |
apps/server/src/provider/Layers/CodexAdapter.ts+1| ... | ... | @@ -1354,6 +1354,7 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => |
| 1354 | 1354 | threadId: input.threadId, |
| 1355 | 1355 | ...(input.input !== undefined ? { input: input.input } : {}), |
| 1356 | 1356 | ...(input.model !== undefined ? { model: input.model } : {}), |
| 1357 | ...(input.serviceTier !== undefined ? { serviceTier: input.serviceTier } : {}), | |
| 1357 | 1358 | ...(input.modelOptions?.codex?.reasoningEffort !== undefined |
| 1358 | 1359 | ? { effort: input.modelOptions.codex.reasoningEffort } |
| 1359 | 1360 | : {}), |
apps/web/src/appSettings.test.ts+21| ... | ... | @@ -4,6 +4,8 @@ import { |
| 4 | 4 | getAppModelOptions, |
| 5 | 5 | getSlashModelOptions, |
| 6 | 6 | normalizeCustomModelSlugs, |
| 7 | resolveAppServiceTier, | |
| 8 | shouldShowFastTierIcon, | |
| 7 | 9 | resolveAppModelSelection, |
| 8 | 10 | } from "./appSettings"; |
| 9 | 11 | |
| ... | ... | @@ -82,3 +84,22 @@ describe("getSlashModelOptions", () => { |
| 82 | 84 | expect(options.map((option) => option.slug)).toEqual(["openai/gpt-oss-120b"]); |
| 83 | 85 | }); |
| 84 | 86 | }); |
| 87 | ||
| 88 | describe("resolveAppServiceTier", () => { | |
| 89 | it("maps automatic to no override", () => { | |
| 90 | expect(resolveAppServiceTier("auto")).toBeNull(); | |
| 91 | }); | |
| 92 | ||
| 93 | it("preserves explicit service tier overrides", () => { | |
| 94 | expect(resolveAppServiceTier("fast")).toBe("fast"); | |
| 95 | expect(resolveAppServiceTier("flex")).toBe("flex"); | |
| 96 | }); | |
| 97 | }); | |
| 98 | ||
| 99 | describe("shouldShowFastTierIcon", () => { | |
| 100 | it("shows the fast-tier icon only for gpt-5.4 on fast tier", () => { | |
| 101 | expect(shouldShowFastTierIcon("gpt-5.4", "fast")).toBe(true); | |
| 102 | expect(shouldShowFastTierIcon("gpt-5.4", "auto")).toBe(false); | |
| 103 | expect(shouldShowFastTierIcon("gpt-5.3-codex", "fast")).toBe(false); | |
| 104 | }); | |
| 105 | }); |
apps/web/src/appSettings.ts+38-1| ... | ... | @@ -1,11 +1,31 @@ |
| 1 | 1 | import { useCallback, useSyncExternalStore } from "react"; |
| 2 | 2 | import { Option, Schema } from "effect"; |
| 3 | import { type ProviderKind } from "@t3tools/contracts"; | |
| 3 | import { type ProviderKind, type ProviderServiceTier } from "@t3tools/contracts"; | |
| 4 | 4 | import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; |
| 5 | 5 | |
| 6 | 6 | const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1"; |
| 7 | 7 | const MAX_CUSTOM_MODEL_COUNT = 32; |
| 8 | 8 | export const MAX_CUSTOM_MODEL_LENGTH = 256; |
| 9 | export const APP_SERVICE_TIER_OPTIONS = [ | |
| 10 | { | |
| 11 | value: "auto", | |
| 12 | label: "Automatic", | |
| 13 | description: "Use Codex defaults without forcing a service tier.", | |
| 14 | }, | |
| 15 | { | |
| 16 | value: "fast", | |
| 17 | label: "Fast", | |
| 18 | description: "Request the fast service tier when the model supports it.", | |
| 19 | }, | |
| 20 | { | |
| 21 | value: "flex", | |
| 22 | label: "Flex", | |
| 23 | description: "Request the flex service tier when the model supports it.", | |
| 24 | }, | |
| 25 | ] as const; | |
| 26 | export type AppServiceTier = (typeof APP_SERVICE_TIER_OPTIONS)[number]["value"]; | |
| 27 | const AppServiceTierSchema = Schema.Literals(["auto", "fast", "flex"]); | |
| 28 | const MODELS_WITH_FAST_SUPPORT = new Set(["gpt-5.4"]); | |
| 9 | 29 | const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record<ProviderKind, ReadonlySet<string>> = { |
| 10 | 30 | codex: new Set(getModelOptions("codex").map((option) => option.slug)), |
| 11 | 31 | }; |
| ... | ... | @@ -21,6 +41,7 @@ const AppSettingsSchema = Schema.Struct({ |
| 21 | 41 | enableAssistantStreaming: Schema.Boolean.pipe( |
| 22 | 42 | Schema.withConstructorDefault(() => Option.some(false)), |
| 23 | 43 | ), |
| 44 | codexServiceTier: AppServiceTierSchema.pipe(Schema.withConstructorDefault(() => Option.some("auto"))), | |
| 24 | 45 | customCodexModels: Schema.Array(Schema.String).pipe( |
| 25 | 46 | Schema.withConstructorDefault(() => Option.some([])), |
| 26 | 47 | ), |
| ... | ... | @@ -32,6 +53,22 @@ export interface AppModelOption { |
| 32 | 53 | isCustom: boolean; |
| 33 | 54 | } |
| 34 | 55 | |
| 56 | export function resolveAppServiceTier(serviceTier: AppServiceTier): ProviderServiceTier | null { | |
| 57 | return serviceTier === "auto" ? null : serviceTier; | |
| 58 | } | |
| 59 | ||
| 60 | export function shouldShowFastTierIcon( | |
| 61 | model: string | null | undefined, | |
| 62 | serviceTier: AppServiceTier, | |
| 63 | ): boolean { | |
| 64 | const normalizedModel = normalizeModelSlug(model); | |
| 65 | return ( | |
| 66 | resolveAppServiceTier(serviceTier) === "fast" && | |
| 67 | normalizedModel !== null && | |
| 68 | MODELS_WITH_FAST_SUPPORT.has(normalizedModel) | |
| 69 | ); | |
| 70 | } | |
| 71 | ||
| 35 | 72 | const DEFAULT_APP_SETTINGS = AppSettingsSchema.makeUnsafe({}); |
| 36 | 73 | |
| 37 | 74 | let listeners: Array<() => void> = []; |
apps/web/src/components/ChatView.tsx+31-3| ... | ... | @@ -140,6 +140,7 @@ import { |
| 140 | 140 | XIcon, |
| 141 | 141 | CopyIcon, |
| 142 | 142 | CheckIcon, |
| 143 | ZapIcon, | |
| 143 | 144 | } from "lucide-react"; |
| 144 | 145 | import { Button } from "./ui/button"; |
| 145 | 146 | import { Input } from "./ui/input"; |
| ... | ... | @@ -196,7 +197,14 @@ import { Toggle } from "./ui/toggle"; |
| 196 | 197 | import { SidebarTrigger } from "./ui/sidebar"; |
| 197 | 198 | import { newCommandId, newMessageId, newThreadId } from "~/lib/utils"; |
| 198 | 199 | import { readNativeApi } from "~/nativeApi"; |
| 199 | import { getAppModelOptions, resolveAppModelSelection, useAppSettings } from "../appSettings"; | |
| 200 | import { | |
| 201 | getAppModelOptions, | |
| 202 | resolveAppModelSelection, | |
| 203 | resolveAppServiceTier, | |
| 204 | shouldShowFastTierIcon, | |
| 205 | type AppServiceTier, | |
| 206 | useAppSettings, | |
| 207 | } from "../appSettings"; | |
| 200 | 208 | import { |
| 201 | 209 | type ComposerImageAttachment, |
| 202 | 210 | type DraftThreadEnvMode, |
| ... | ... | @@ -388,6 +396,7 @@ type ComposerCommandItem = |
| 388 | 396 | model: ModelSlug; |
| 389 | 397 | label: string; |
| 390 | 398 | description: string; |
| 399 | showFastBadge: boolean; | |
| 391 | 400 | }; |
| 392 | 401 | |
| 393 | 402 | type SendPhase = "idle" | "preparing-worktree" | "sending-turn"; |
| ... | ... | @@ -497,7 +506,12 @@ const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: { |
| 497 | 506 | model |
| 498 | 507 | </Badge> |
| 499 | 508 | ) : null} |
| 500 | <span className="truncate">{props.item.label}</span> | |
| 509 | <span className="flex min-w-0 items-center gap-1.5 truncate"> | |
| 510 | {props.item.type === "model" && props.item.showFastBadge ? ( | |
| 511 | <ZapIcon className="size-3.5 shrink-0 text-amber-500" /> | |
| 512 | ) : null} | |
| 513 | <span className="truncate">{props.item.label}</span> | |
| 514 | </span> | |
| 501 | 515 | <span className="truncate text-muted-foreground/70 text-xs">{props.item.description}</span> |
| 502 | 516 | </CommandItem> |
| 503 | 517 | ); |
| ... | ... | @@ -754,6 +768,8 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 754 | 768 | activeThread.messages.length > 0 || |
| 755 | 769 | activeThread.session !== null), |
| 756 | 770 | ); |
| 771 | const selectedServiceTierSetting = settings.codexServiceTier; | |
| 772 | const selectedServiceTier = resolveAppServiceTier(selectedServiceTierSetting); | |
| 757 | 773 | const lockedProvider: ProviderKind | null = hasThreadStarted |
| 758 | 774 | ? (sessionProvider ?? selectedProviderByThreadId ?? null) |
| 759 | 775 | : null; |
| ... | ... | @@ -1205,8 +1221,10 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1205 | 1221 | model: slug, |
| 1206 | 1222 | label: name, |
| 1207 | 1223 | description: `${providerLabel} · ${slug}`, |
| 1224 | showFastBadge: | |
| 1225 | provider === "codex" && shouldShowFastTierIcon(slug, selectedServiceTierSetting), | |
| 1208 | 1226 | })); |
| 1209 | }, [composerTrigger, searchableModelOptions, workspaceEntries]); | |
| 1227 | }, [composerTrigger, searchableModelOptions, selectedServiceTierSetting, workspaceEntries]); | |
| 1210 | 1228 | const composerMenuOpen = Boolean(composerTrigger); |
| 1211 | 1229 | const activeComposerMenuItem = useMemo( |
| 1212 | 1230 | () => |
| ... | ... | @@ -2539,6 +2557,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2539 | 2557 | attachments: turnAttachments, |
| 2540 | 2558 | }, |
| 2541 | 2559 | model: selectedModel || undefined, |
| 2560 | serviceTier: selectedServiceTier, | |
| 2542 | 2561 | ...(selectedModelOptionsForDispatch |
| 2543 | 2562 | ? { modelOptions: selectedModelOptionsForDispatch } |
| 2544 | 2563 | : {}), |
| ... | ... | @@ -3534,6 +3553,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3534 | 3553 | model={selectedModelForPickerWithCustomFallback} |
| 3535 | 3554 | lockedProvider={lockedProvider} |
| 3536 | 3555 | modelOptionsByProvider={modelOptionsByProvider} |
| 3556 | serviceTierSetting={selectedServiceTierSetting} | |
| 3537 | 3557 | onProviderModelChange={onProviderModelSelect} |
| 3538 | 3558 | /> |
| 3539 | 3559 | |
| ... | ... | @@ -5220,6 +5240,7 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: { |
| 5220 | 5240 | model: ModelSlug; |
| 5221 | 5241 | lockedProvider: ProviderKind | null; |
| 5222 | 5242 | modelOptionsByProvider: Record<ProviderKind, ReadonlyArray<{ slug: string; name: string }>>; |
| 5243 | serviceTierSetting: AppServiceTier; | |
| 5223 | 5244 | disabled?: boolean; |
| 5224 | 5245 | onProviderModelChange: (provider: ProviderKind, model: ModelSlug) => void; |
| 5225 | 5246 | }) { |
| ... | ... | @@ -5252,6 +5273,9 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: { |
| 5252 | 5273 | > |
| 5253 | 5274 | <span className="flex min-w-0 items-center gap-2"> |
| 5254 | 5275 | <ProviderIcon aria-hidden="true" className="size-4 shrink-0 text-muted-foreground/70" /> |
| 5276 | {props.provider === "codex" && shouldShowFastTierIcon(props.model, props.serviceTierSetting) ? ( | |
| 5277 | <ZapIcon className="size-3.5 shrink-0 text-amber-500" /> | |
| 5278 | ) : null} | |
| 5255 | 5279 | <span className="truncate">{selectedModelLabel}</span> |
| 5256 | 5280 | <ChevronDownIcon aria-hidden="true" className="size-3 opacity-60" /> |
| 5257 | 5281 | </span> |
| ... | ... | @@ -5294,6 +5318,10 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: { |
| 5294 | 5318 | value={modelOption.slug} |
| 5295 | 5319 | onClick={() => setIsMenuOpen(false)} |
| 5296 | 5320 | > |
| 5321 | {option.value === "codex" && | |
| 5322 | shouldShowFastTierIcon(modelOption.slug, props.serviceTierSetting) ? ( | |
| 5323 | <ZapIcon className="size-3.5 shrink-0 text-amber-500" /> | |
| 5324 | ) : null} | |
| 5297 | 5325 | {modelOption.name} |
| 5298 | 5326 | </MenuRadioItem> |
| 5299 | 5327 | ))} |
apps/web/src/routes/_chat.settings.tsx+55-5| ... | ... | @@ -3,8 +3,14 @@ import { useQuery } from "@tanstack/react-query"; |
| 3 | 3 | import { useCallback, useState } from "react"; |
| 4 | 4 | import { type ProviderKind } from "@t3tools/contracts"; |
| 5 | 5 | import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; |
| 6 | ||
| 7 | import { MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings"; | |
| 6 | import { ZapIcon } from "lucide-react"; | |
| 7 | ||
| 8 | import { | |
| 9 | APP_SERVICE_TIER_OPTIONS, | |
| 10 | MAX_CUSTOM_MODEL_LENGTH, | |
| 11 | shouldShowFastTierIcon, | |
| 12 | useAppSettings, | |
| 13 | } from "../appSettings"; | |
| 8 | 14 | import { isElectron } from "../env"; |
| 9 | 15 | import { useTheme } from "../hooks/useTheme"; |
| 10 | 16 | import { serverConfigQueryOptions } from "../lib/serverReactQuery"; |
| ... | ... | @@ -12,6 +18,7 @@ import { ensureNativeApi } from "../nativeApi"; |
| 12 | 18 | import { preferredTerminalEditor } from "../terminal-links"; |
| 13 | 19 | import { Button } from "../components/ui/button"; |
| 14 | 20 | import { Input } from "../components/ui/input"; |
| 21 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../components/ui/select"; | |
| 15 | 22 | import { Switch } from "../components/ui/switch"; |
| 16 | 23 | import { SidebarInset } from "~/components/ui/sidebar"; |
| 17 | 24 | |
| ... | ... | @@ -96,6 +103,7 @@ function SettingsRouteView() { |
| 96 | 103 | |
| 97 | 104 | const codexBinaryPath = settings.codexBinaryPath; |
| 98 | 105 | const codexHomePath = settings.codexHomePath; |
| 106 | const codexServiceTier = settings.codexServiceTier; | |
| 99 | 107 | const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null; |
| 100 | 108 | |
| 101 | 109 | const openKeybindingsFile = useCallback(() => { |
| ... | ... | @@ -302,6 +310,43 @@ function SettingsRouteView() { |
| 302 | 310 | </div> |
| 303 | 311 | |
| 304 | 312 | <div className="space-y-5"> |
| 313 | <label className="block space-y-1"> | |
| 314 | <span className="text-xs font-medium text-foreground">Default service tier</span> | |
| 315 | <Select | |
| 316 | items={APP_SERVICE_TIER_OPTIONS.map((option) => ({ | |
| 317 | label: option.label, | |
| 318 | value: option.value, | |
| 319 | }))} | |
| 320 | value={codexServiceTier} | |
| 321 | onValueChange={(value) => { | |
| 322 | if (!value) return; | |
| 323 | updateSettings({ codexServiceTier: value }); | |
| 324 | }} | |
| 325 | > | |
| 326 | <SelectTrigger> | |
| 327 | <SelectValue /> | |
| 328 | </SelectTrigger> | |
| 329 | <SelectPopup alignItemWithTrigger={false}> | |
| 330 | {APP_SERVICE_TIER_OPTIONS.map((option) => ( | |
| 331 | <SelectItem key={option.value} value={option.value}> | |
| 332 | <div className="flex min-w-0 items-center gap-2"> | |
| 333 | {option.value === "fast" ? ( | |
| 334 | <ZapIcon className="size-3.5 text-amber-500" /> | |
| 335 | ) : ( | |
| 336 | <span className="size-3.5 shrink-0" aria-hidden="true" /> | |
| 337 | )} | |
| 338 | <span className="truncate">{option.label}</span> | |
| 339 | </div> | |
| 340 | </SelectItem> | |
| 341 | ))} | |
| 342 | </SelectPopup> | |
| 343 | </Select> | |
| 344 | <span className="text-xs text-muted-foreground"> | |
| 345 | {APP_SERVICE_TIER_OPTIONS.find((option) => option.value === codexServiceTier) | |
| 346 | ?.description ?? "Use Codex defaults without forcing a service tier."} | |
| 347 | </span> | |
| 348 | </label> | |
| 349 | ||
| 305 | 350 | {MODEL_PROVIDER_SETTINGS.map((providerSettings) => { |
| 306 | 351 | const provider = providerSettings.provider; |
| 307 | 352 | const customModels = getCustomModelsForProvider(settings, provider); |
| ... | ... | @@ -400,9 +445,14 @@ function SettingsRouteView() { |
| 400 | 445 | key={`${provider}:${slug}`} |
| 401 | 446 | className="flex items-center justify-between gap-3 rounded-lg border border-border bg-background px-3 py-2" |
| 402 | 447 | > |
| 403 | <code className="min-w-0 flex-1 truncate text-xs text-foreground"> | |
| 404 | {slug} | |
| 405 | </code> | |
| 448 | <div className="flex min-w-0 flex-1 items-center gap-2"> | |
| 449 | {provider === "codex" && shouldShowFastTierIcon(slug, codexServiceTier) ? ( | |
| 450 | <ZapIcon className="size-3.5 shrink-0 text-amber-500" /> | |
| 451 | ) : null} | |
| 452 | <code className="min-w-0 flex-1 truncate text-xs text-foreground"> | |
| 453 | {slug} | |
| 454 | </code> | |
| 455 | </div> | |
| 406 | 456 | <Button |
| 407 | 457 | size="xs" |
| 408 | 458 | variant="ghost" |
packages/contracts/src/orchestration.ts+18| ... | ... | @@ -29,6 +29,21 @@ export const ORCHESTRATION_WS_CHANNELS = { |
| 29 | 29 | |
| 30 | 30 | export const ProviderKind = Schema.Literal("codex"); |
| 31 | 31 | export type ProviderKind = typeof ProviderKind.Type; |
| 32 | export const ProviderApprovalPolicy = Schema.Literals([ | |
| 33 | "untrusted", | |
| 34 | "on-failure", | |
| 35 | "on-request", | |
| 36 | "never", | |
| 37 | ]); | |
| 38 | export type ProviderApprovalPolicy = typeof ProviderApprovalPolicy.Type; | |
| 39 | export const ProviderSandboxMode = Schema.Literals([ | |
| 40 | "read-only", | |
| 41 | "workspace-write", | |
| 42 | "danger-full-access", | |
| 43 | ]); | |
| 44 | export type ProviderSandboxMode = typeof ProviderSandboxMode.Type; | |
| 45 | export const ProviderServiceTier = Schema.Literals(["fast", "flex"]); | |
| 46 | export type ProviderServiceTier = typeof ProviderServiceTier.Type; | |
| 32 | 47 | export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex"; |
| 33 | 48 | export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); |
| 34 | 49 | export type RuntimeMode = typeof RuntimeMode.Type; |
| ... | ... | @@ -349,6 +364,7 @@ export const ThreadTurnStartCommand = Schema.Struct({ |
| 349 | 364 | }), |
| 350 | 365 | provider: Schema.optional(ProviderKind), |
| 351 | 366 | model: Schema.optional(TrimmedNonEmptyString), |
| 367 | serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), | |
| 352 | 368 | modelOptions: Schema.optional(ProviderModelOptions), |
| 353 | 369 | assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), |
| 354 | 370 | runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)), |
| ... | ... | @@ -370,6 +386,7 @@ const ClientThreadTurnStartCommand = Schema.Struct({ |
| 370 | 386 | }), |
| 371 | 387 | provider: Schema.optional(ProviderKind), |
| 372 | 388 | model: Schema.optional(TrimmedNonEmptyString), |
| 389 | serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), | |
| 373 | 390 | modelOptions: Schema.optional(ProviderModelOptions), |
| 374 | 391 | assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), |
| 375 | 392 | runtimeMode: RuntimeMode, |
| ... | ... | @@ -649,6 +666,7 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({ |
| 649 | 666 | messageId: MessageId, |
| 650 | 667 | provider: Schema.optional(ProviderKind), |
| 651 | 668 | model: Schema.optional(TrimmedNonEmptyString), |
| 669 | serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), | |
| 652 | 670 | modelOptions: Schema.optional(ProviderModelOptions), |
| 653 | 671 | assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), |
| 654 | 672 | runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)), |
packages/contracts/src/provider.ts+7| ... | ... | @@ -14,9 +14,12 @@ import { |
| 14 | 14 | PROVIDER_SEND_TURN_MAX_ATTACHMENTS, |
| 15 | 15 | PROVIDER_SEND_TURN_MAX_INPUT_CHARS, |
| 16 | 16 | ProviderApprovalDecision, |
| 17 | ProviderApprovalPolicy, | |
| 17 | 18 | ProviderInteractionMode, |
| 18 | 19 | ProviderKind, |
| 19 | 20 | ProviderRequestKind, |
| 21 | ProviderSandboxMode, | |
| 22 | ProviderServiceTier, | |
| 20 | 23 | ProviderUserInputAnswers, |
| 21 | 24 | RuntimeMode, |
| 22 | 25 | } from "./orchestration"; |
| ... | ... | @@ -61,6 +64,9 @@ export const ProviderSessionStartInput = Schema.Struct({ |
| 61 | 64 | model: Schema.optional(TrimmedNonEmptyStringSchema), |
| 62 | 65 | modelOptions: Schema.optional(ProviderModelOptions), |
| 63 | 66 | resumeCursor: Schema.optional(Schema.Unknown), |
| 67 | serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), | |
| 68 | approvalPolicy: Schema.optional(ProviderApprovalPolicy), | |
| 69 | sandboxMode: Schema.optional(ProviderSandboxMode), | |
| 64 | 70 | providerOptions: Schema.optional(ProviderStartOptions), |
| 65 | 71 | runtimeMode: RuntimeMode, |
| 66 | 72 | }); |
| ... | ... | @@ -75,6 +81,7 @@ export const ProviderSendTurnInput = Schema.Struct({ |
| 75 | 81 | Schema.Array(ChatAttachment).check(Schema.isMaxLength(PROVIDER_SEND_TURN_MAX_ATTACHMENTS)), |
| 76 | 82 | ), |
| 77 | 83 | model: Schema.optional(TrimmedNonEmptyStringSchema), |
| 84 | serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), | |
| 78 | 85 | modelOptions: Schema.optional(ProviderModelOptions), |
| 79 | 86 | interactionMode: Schema.optional(ProviderInteractionMode), |
| 80 | 87 | }); |