| author | |
| committer | |
| log | a92161a05040c3fcaaa479f1b60d2d45cd368dd4 |
| tree | 35c96cf72500d309d7aceb63ae49aa8331c2e758 |
| parent | cd64ad384e40988f57e4caa5a3f74f25ca5ef450 |
| signature | Signed by PGP key B5690EEEBB952194 |
8 files changed, 128 insertions(+), 14 deletions(-)
apps/server/src/provider/Drivers/CodexDriver.ts+5-1| ... | @@ -186,7 +186,6 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { | ... | @@ -186,7 +186,6 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { |
| 186 | environment: processEnv, | 186 | environment: processEnv, |
| 187 | ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), | 187 | ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), |
| 188 | }); | 188 | }); |
| 189 | const textGeneration = yield* makeCodexTextGeneration(effectiveConfig, processEnv); | ||
| 190 | 189 | ||
| 191 | // Build a managed snapshot whose settings never change — mutations come | 190 | // Build a managed snapshot whose settings never change — mutations come |
| 192 | // in as instance rebuilds from the registry rather than in-place | 191 | // in as instance rebuilds from the registry rather than in-place |
| ... | @@ -242,6 +241,11 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { | ... | @@ -242,6 +241,11 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { |
| 242 | }), | 241 | }), |
| 243 | ), | 242 | ), |
| 244 | ); | 243 | ); |
| 244 | const textGeneration = yield* makeCodexTextGeneration( | ||
| 245 | effectiveConfig, | ||
| 246 | processEnv, | ||
| 247 | snapshot.getSnapshot.pipe(Effect.map((value) => value.models)), | ||
| 248 | ); | ||
| 245 | const snapshotForCwd = (cwd: string) => | 249 | const snapshotForCwd = (cwd: string) => |
| 246 | !effectiveConfig.enabled | 250 | !effectiveConfig.enabled |
| 247 | ? snapshot.getSnapshot | 251 | ? snapshot.getSnapshot |
apps/server/src/provider/Layers/CodexProvider.test.ts+17| ... | @@ -127,6 +127,23 @@ it("prefers sol over terra when both are available", () => { | ... | @@ -127,6 +127,23 @@ it("prefers sol over terra when both are available", () => { |
| 127 | assert.deepStrictEqual(models.find((model) => model.isDefault)?.slug, "gpt-5.6-sol"); | 127 | assert.deepStrictEqual(models.find((model) => model.isDefault)?.slug, "gpt-5.6-sol"); |
| 128 | }); | 128 | }); |
| 129 | 129 | ||
| 130 | it("ranks qualified Codex models while preserving their wire ids", () => { | ||
| 131 | const models = applyPreferredCodexDefaultModel([ | ||
| 132 | { | ||
| 133 | slug: "openai.gpt-5.6-luna", | ||
| 134 | name: "Luna", | ||
| 135 | isCustom: false, | ||
| 136 | isDefault: true, | ||
| 137 | capabilities: null, | ||
| 138 | }, | ||
| 139 | { slug: "openai.gpt-5.6-sol", name: "Sol", isCustom: false, capabilities: null }, | ||
| 140 | ]); | ||
| 141 | assert.deepStrictEqual( | ||
| 142 | models.filter((model) => model.isDefault).map((model) => model.slug), | ||
| 143 | ["openai.gpt-5.6-sol"], | ||
| 144 | ); | ||
| 145 | }); | ||
| 146 | |||
| 130 | it("keeps Codex's own default when no preferred model is available", () => { | 147 | it("keeps Codex's own default when no preferred model is available", () => { |
| 131 | const models = applyPreferredCodexDefaultModel([ | 148 | const models = applyPreferredCodexDefaultModel([ |
| 132 | { slug: "gpt-5.5", name: "GPT-5.5", isCustom: false, capabilities: null }, | 149 | { slug: "gpt-5.5", name: "GPT-5.5", isCustom: false, capabilities: null }, |
apps/server/src/provider/Layers/CodexProvider.ts+8-4| ... | @@ -25,7 +25,11 @@ import type { | ... | @@ -25,7 +25,11 @@ import type { |
| 25 | } from "@t3tools/contracts"; | 25 | } from "@t3tools/contracts"; |
| 26 | import { PREFERRED_DEFAULT_CODEX_MODELS, ServerSettingsError } from "@t3tools/contracts"; | 26 | import { PREFERRED_DEFAULT_CODEX_MODELS, ServerSettingsError } from "@t3tools/contracts"; |
| 27 | 27 | ||
| 28 | import { createModelCapabilities, readCustomModelEntries } from "@t3tools/shared/model"; | 28 | import { |
| 29 | codexModelFamily, | ||
| 30 | createModelCapabilities, | ||
| 31 | readCustomModelEntries, | ||
| 32 | } from "@t3tools/shared/model"; | ||
| 29 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; | 33 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; |
| 30 | import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts"; | 34 | import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts"; |
| 31 | import { | 35 | import { |
| ... | @@ -232,9 +236,9 @@ function parseCodexModelListResponse( | ... | @@ -232,9 +236,9 @@ function parseCodexModelListResponse( |
| 232 | export function applyPreferredCodexDefaultModel( | 236 | export function applyPreferredCodexDefaultModel( |
| 233 | models: ReadonlyArray<ServerProviderModel>, | 237 | models: ReadonlyArray<ServerProviderModel>, |
| 234 | ): ReadonlyArray<ServerProviderModel> { | 238 | ): ReadonlyArray<ServerProviderModel> { |
| 235 | const preferredSlug = PREFERRED_DEFAULT_CODEX_MODELS.find((slug) => | 239 | const preferredSlug = PREFERRED_DEFAULT_CODEX_MODELS.flatMap((slug) => |
| 236 | models.some((model) => model.slug === slug && !model.isCustom), | 240 | models.filter((model) => !model.isCustom && codexModelFamily(model.slug) === slug), |
| 237 | ); | 241 | )[0]?.slug; |
| 238 | if (!preferredSlug) { | 242 | if (!preferredSlug) { |
| 239 | return models; | 243 | return models; |
| 240 | } | 244 | } |
apps/server/src/provider/ModelManifest.test.ts+29| ... | @@ -38,6 +38,20 @@ const model = (overrides: Partial<ServerProviderModel>): ServerProviderModel => | ... | @@ -38,6 +38,20 @@ const model = (overrides: Partial<ServerProviderModel>): ServerProviderModel => |
| 38 | }); | 38 | }); |
| 39 | 39 | ||
| 40 | describe("classifyModels", () => { | 40 | describe("classifyModels", () => { |
| 41 | it("classifies qualified Codex families without changing their wire ids", () => { | ||
| 42 | const manifest: ModelManifestData = { version: 1, currentModels: { codex: ["gpt-test"] } }; | ||
| 43 | const models = [ | ||
| 44 | model({ slug: "openai.gpt-test", isLegacy: true }), | ||
| 45 | model({ slug: "openai.gpt-old" }), | ||
| 46 | ]; | ||
| 47 | assert.deepStrictEqual( | ||
| 48 | classifyModels(models, manifest, CODEX).map((entry) => [entry.slug, entry.isLegacy ?? false]), | ||
| 49 | [ | ||
| 50 | ["openai.gpt-test", false], | ||
| 51 | ["openai.gpt-old", true], | ||
| 52 | ], | ||
| 53 | ); | ||
| 54 | }); | ||
| 41 | it("flags non-current models, clears stale flags, and skips custom models", () => { | 55 | it("flags non-current models, clears stale flags, and skips custom models", () => { |
| 42 | const manifest: ModelManifestData = { | 56 | const manifest: ModelManifestData = { |
| 43 | version: 1, | 57 | version: 1, |
| ... | @@ -64,6 +78,21 @@ describe("classifyModels", () => { | ... | @@ -64,6 +78,21 @@ describe("classifyModels", () => { |
| 64 | }); | 78 | }); |
| 65 | 79 | ||
| 66 | describe("applyManifestDefault", () => { | 80 | describe("applyManifestDefault", () => { |
| 81 | it("resolves the manifest default to the qualified live model", () => { | ||
| 82 | const manifest: ModelManifestData = { | ||
| 83 | version: 1, | ||
| 84 | currentModels: {}, | ||
| 85 | providers: { codex: { models: [], profiles: {}, defaults: { chat: "gpt-test" } } }, | ||
| 86 | }; | ||
| 87 | const models = [ | ||
| 88 | model({ slug: "openai.gpt-old", isDefault: true }), | ||
| 89 | model({ slug: "openai.gpt-test" }), | ||
| 90 | ]; | ||
| 91 | assert.strictEqual( | ||
| 92 | applyManifestDefault(models, manifest, CODEX).find((entry) => entry.isDefault)?.slug, | ||
| 93 | "openai.gpt-test", | ||
| 94 | ); | ||
| 95 | }); | ||
| 67 | it("moves the default flag and its aliases to the manifest's chat default", () => { | 96 | it("moves the default flag and its aliases to the manifest's chat default", () => { |
| 68 | const driver = ProviderDriverKind.make("antigravity"); | 97 | const driver = ProviderDriverKind.make("antigravity"); |
| 69 | const manifest: ModelManifestData = { | 98 | const manifest: ModelManifestData = { |
apps/server/src/provider/ModelManifest.ts+18-6| ... | @@ -19,6 +19,7 @@ import { | ... | @@ -19,6 +19,7 @@ import { |
| 19 | type ProviderDriverKind, | 19 | type ProviderDriverKind, |
| 20 | type ServerProviderModel, | 20 | type ServerProviderModel, |
| 21 | } from "@t3tools/contracts"; | 21 | } from "@t3tools/contracts"; |
| 22 | import { codexModelFamily } from "@t3tools/shared/model"; | ||
| 22 | import * as Clock from "effect/Clock"; | 23 | import * as Clock from "effect/Clock"; |
| 23 | import * as Context from "effect/Context"; | 24 | import * as Context from "effect/Context"; |
| 24 | import * as Effect from "effect/Effect"; | 25 | import * as Effect from "effect/Effect"; |
| ... | @@ -212,13 +213,15 @@ function isLegacyModel( | ... | @@ -212,13 +213,15 @@ function isLegacyModel( |
| 212 | driverKind: ProviderDriverKind, | 213 | driverKind: ProviderDriverKind, |
| 213 | slug: string, | 214 | slug: string, |
| 214 | ): boolean { | 215 | ): boolean { |
| 215 | const catalogModel = manifest.providers?.[driverKind]?.models.find( | 216 | const family = driverKind === "codex" ? codexModelFamily(slug) : slug; |
| 216 | (model) => model.slug === slug, | 217 | const catalog = manifest.providers?.[driverKind]?.models; |
| 217 | ); | 218 | const catalogModel = |
| 219 | catalog?.find((model) => model.slug === slug) ?? | ||
| 220 | catalog?.find((model) => model.slug === family); | ||
| 218 | if (catalogModel) return catalogModel.status === "legacy"; | 221 | if (catalogModel) return catalogModel.status === "legacy"; |
| 219 | const currentModels = manifest.currentModels[driverKind]; | 222 | const currentModels = manifest.currentModels[driverKind]; |
| 220 | if (!currentModels) return false; | 223 | if (!currentModels) return false; |
| 221 | return !currentModels.includes(slug); | 224 | return !currentModels.includes(slug) && !currentModels.includes(family); |
| 222 | } | 225 | } |
| 223 | 226 | ||
| 224 | /** | 227 | /** |
| ... | @@ -260,8 +263,17 @@ export function applyManifestDefault( | ... | @@ -260,8 +263,17 @@ export function applyManifestDefault( |
| 260 | manifest: ModelManifestData, | 263 | manifest: ModelManifestData, |
| 261 | driverKind: ProviderDriverKind, | 264 | driverKind: ProviderDriverKind, |
| 262 | ): ReadonlyArray<ServerProviderModel> { | 265 | ): ReadonlyArray<ServerProviderModel> { |
| 263 | const slug = manifestDefaultModel(manifest, driverKind); | 266 | const requestedSlug = manifestDefaultModel(manifest, driverKind); |
| 264 | if (slug === undefined || !models.some((model) => model.slug === slug)) return models; | 267 | if (requestedSlug === undefined) return models; |
| 268 | const slug = | ||
| 269 | models.find((model) => model.slug === requestedSlug)?.slug ?? | ||
| 270 | (driverKind === "codex" | ||
| 271 | ? models.find( | ||
| 272 | (model) => | ||
| 273 | !model.isCustom && codexModelFamily(model.slug) === codexModelFamily(requestedSlug), | ||
| 274 | )?.slug | ||
| 275 | : undefined); | ||
| 276 | if (slug === undefined) return models; | ||
| 265 | const previous = models.find((model) => model.isDefault && model.slug !== slug); | 277 | const previous = models.find((model) => model.isDefault && model.slug !== slug); |
| 266 | if (!previous) return models; | 278 | if (!previous) return models; |
| 267 | const movedAliases = previous.aliases ?? []; | 279 | const movedAliases = previous.aliases ?? []; |
apps/server/src/textGeneration/CodexTextGeneration.test.ts+34-1| ... | @@ -134,6 +134,7 @@ function withFakeCodexEnv<A, E, R>( | ... | @@ -134,6 +134,7 @@ function withFakeCodexEnv<A, E, R>( |
| 134 | input: FakeCodexInput & { | 134 | input: FakeCodexInput & { |
| 135 | launchArgs?: string; | 135 | launchArgs?: string; |
| 136 | environment?: NodeJS.ProcessEnv; | 136 | environment?: NodeJS.ProcessEnv; |
| 137 | models?: ReadonlyArray<string>; | ||
| 137 | }, | 138 | }, |
| 138 | effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>, | 139 | effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>, |
| 139 | ) { | 140 | ) { |
| ... | @@ -142,12 +143,44 @@ function withFakeCodexEnv<A, E, R>( | ... | @@ -142,12 +143,44 @@ function withFakeCodexEnv<A, E, R>( |
| 142 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-codex-text-" }); | 143 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-codex-text-" }); |
| 143 | const codexPath = yield* makeFakeCodexBinary(tempDir, input); | 144 | const codexPath = yield* makeFakeCodexBinary(tempDir, input); |
| 144 | const config = decodeCodexSettings({ binaryPath: codexPath, launchArgs: input.launchArgs }); | 145 | const config = decodeCodexSettings({ binaryPath: codexPath, launchArgs: input.launchArgs }); |
| 145 | const textGeneration = yield* makeCodexTextGeneration(config, input.environment); | 146 | const textGeneration = yield* makeCodexTextGeneration( |
| 147 | config, | ||
| 148 | input.environment, | ||
| 149 | Effect.succeed( | ||
| 150 | (input.models ?? []).map((slug) => ({ | ||
| 151 | slug, | ||
| 152 | name: slug, | ||
| 153 | isCustom: false, | ||
| 154 | capabilities: null, | ||
| 155 | })), | ||
| 156 | ), | ||
| 157 | ); | ||
| 146 | return yield* effectFn(textGeneration); | 158 | return yield* effectFn(textGeneration); |
| 147 | }).pipe(Effect.scoped); | 159 | }).pipe(Effect.scoped); |
| 148 | } | 160 | } |
| 149 | 161 | ||
| 150 | it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => { | 162 | it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => { |
| 163 | for (const selectedModel of ["gpt-5.6-luna", "openai.gpt-5.6-luna"]) { | ||
| 164 | it.effect(`dispatches the qualified live model for ${selectedModel}`, () => | ||
| 165 | withFakeCodexEnv( | ||
| 166 | { | ||
| 167 | output: JSON.stringify({ title: "Bedrock title" }), | ||
| 168 | models: ["openai.gpt-5.6-luna"], | ||
| 169 | requireArg: "--model openai.gpt-5.6-luna", | ||
| 170 | forbidArg: "--model gpt-5.6-luna", | ||
| 171 | }, | ||
| 172 | (textGeneration) => | ||
| 173 | Effect.gen(function* () { | ||
| 174 | const result = yield* textGeneration.generateThreadTitle({ | ||
| 175 | cwd: process.cwd(), | ||
| 176 | message: "Describe this change", | ||
| 177 | modelSelection: createModelSelection(ProviderInstanceId.make("codex"), selectedModel), | ||
| 178 | }); | ||
| 179 | expect(result.title).toBe("Bedrock title"); | ||
| 180 | }), | ||
| 181 | ), | ||
| 182 | ); | ||
| 183 | } | ||
| 151 | it.effect("generates and sanitizes commit messages without branch by default", () => | 184 | it.effect("generates and sanitizes commit messages without branch by default", () => |
| 152 | withFakeCodexEnv( | 185 | withFakeCodexEnv( |
| 153 | { | 186 | { |
apps/server/src/textGeneration/CodexTextGeneration.ts+12-2| ... | @@ -11,6 +11,7 @@ import { | ... | @@ -11,6 +11,7 @@ import { |
| 11 | type CodexSettings, | 11 | type CodexSettings, |
| 12 | DEFAULT_TEXT_GENERATION_REASONING_EFFORT, | 12 | DEFAULT_TEXT_GENERATION_REASONING_EFFORT, |
| 13 | type ModelSelection, | 13 | type ModelSelection, |
| 14 | type ServerProviderModel, | ||
| 14 | TextGenerationError, | 15 | TextGenerationError, |
| 15 | } from "@t3tools/contracts"; | 16 | } from "@t3tools/contracts"; |
| 16 | import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; | 17 | import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; |
| ... | @@ -34,7 +35,7 @@ import { | ... | @@ -34,7 +35,7 @@ import { |
| 34 | sanitizeThreadTitle, | 35 | sanitizeThreadTitle, |
| 35 | toJsonSchemaObject, | 36 | toJsonSchemaObject, |
| 36 | } from "./TextGenerationUtils.ts"; | 37 | } from "./TextGenerationUtils.ts"; |
| 37 | import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; | 38 | import { codexModelFamily, getModelSelectionStringOptionValue } from "@t3tools/shared/model"; |
| 38 | import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts"; | 39 | import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts"; |
| 39 | 40 | ||
| 40 | const CODEX_TIMEOUT_MS = 180_000; | 41 | const CODEX_TIMEOUT_MS = 180_000; |
| ... | @@ -46,6 +47,7 @@ const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknow | ... | @@ -46,6 +47,7 @@ const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknow |
| 46 | export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(function* ( | 47 | export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(function* ( |
| 47 | codexConfig: CodexSettings, | 48 | codexConfig: CodexSettings, |
| 48 | environment?: NodeJS.ProcessEnv, | 49 | environment?: NodeJS.ProcessEnv, |
| 50 | getModels: Effect.Effect<ReadonlyArray<ServerProviderModel>> = Effect.succeed([]), | ||
| 49 | ) { | 51 | ) { |
| 50 | const fileSystem = yield* FileSystem.FileSystem; | 52 | const fileSystem = yield* FileSystem.FileSystem; |
| 51 | const path = yield* Path.Path; | 53 | const path = yield* Path.Path; |
| ... | @@ -178,6 +180,14 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func | ... | @@ -178,6 +180,14 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 178 | const outputPath = yield* writeTempFile(operation, "codex-output", ""); | 180 | const outputPath = yield* writeTempFile(operation, "codex-output", ""); |
| 179 | 181 | ||
| 180 | const runCodexCommand = Effect.fn("runCodexJson.runCodexCommand")(function* () { | 182 | const runCodexCommand = Effect.fn("runCodexJson.runCodexCommand")(function* () { |
| 183 | const models = yield* getModels; | ||
| 184 | const requestedModel = modelSelection.model; | ||
| 185 | const model = | ||
| 186 | models.find((candidate) => candidate.slug === requestedModel)?.slug ?? | ||
| 187 | models.find( | ||
| 188 | (candidate) => !candidate.isCustom && codexModelFamily(candidate.slug) === requestedModel, | ||
| 189 | )?.slug ?? | ||
| 190 | requestedModel; | ||
| 181 | const launchArgs = resolveCodexLaunchArgs(codexConfig.launchArgs, resolvedEnvironment); | 191 | const launchArgs = resolveCodexLaunchArgs(codexConfig.launchArgs, resolvedEnvironment); |
| 182 | const reasoningEffort = | 192 | const reasoningEffort = |
| 183 | getModelSelectionStringOptionValue(modelSelection, "reasoningEffort") ?? | 193 | getModelSelectionStringOptionValue(modelSelection, "reasoningEffort") ?? |
| ... | @@ -193,7 +203,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func | ... | @@ -193,7 +203,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 193 | "-s", | 203 | "-s", |
| 194 | "read-only", | 204 | "read-only", |
| 195 | "--model", | 205 | "--model", |
| 196 | modelSelection.model, | 206 | model, |
| 197 | "--config", | 207 | "--config", |
| 198 | `model_reasoning_effort="${reasoningEffort}"`, | 208 | `model_reasoning_effort="${reasoningEffort}"`, |
| 199 | ...(serviceTier ? ["--config", `service_tier="${serviceTier}"`] : []), | 209 | ...(serviceTier ? ["--config", `service_tier="${serviceTier}"`] : []), |
packages/shared/src/model.ts+5| ... | @@ -225,6 +225,11 @@ export function isClaudeUltrathinkPrompt(text: string | null | undefined): boole | ... | @@ -225,6 +225,11 @@ export function isClaudeUltrathinkPrompt(text: string | null | undefined): boole |
| 225 | return typeof text === "string" && /\bultrathink\b/i.test(text); | 225 | return typeof text === "string" && /\bultrathink\b/i.test(text); |
| 226 | } | 226 | } |
| 227 | 227 | ||
| 228 | /** Compare Codex model families without changing provider-owned dispatch identifiers. */ | ||
| 229 | export function codexModelFamily(slug: string): string { | ||
| 230 | return slug.startsWith("openai.gpt-") ? slug.slice("openai.".length) : slug; | ||
| 231 | } | ||
| 232 | |||
| 228 | export function normalizeModelSlug( | 233 | export function normalizeModelSlug( |
| 229 | model: string | null | undefined, | 234 | model: string | null | undefined, |
| 230 | provider: ProviderDriverKind = DEFAULT_PROVIDER_DRIVER_KIND, | 235 | provider: ProviderDriverKind = DEFAULT_PROVIDER_DRIVER_KIND, |