| 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 | 186 | environment: processEnv, |
| 187 | 187 | ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), |
| 188 | 188 | }); |
| 189 | const textGeneration = yield* makeCodexTextGeneration(effectiveConfig, processEnv); | |
| 190 | 189 | |
| 191 | 190 | // Build a managed snapshot whose settings never change — mutations come |
| 192 | 191 | // in as instance rebuilds from the registry rather than in-place |
| ... | ... | @@ -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 | 249 | const snapshotForCwd = (cwd: string) => |
| 246 | 250 | !effectiveConfig.enabled |
| 247 | 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 | 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 | 147 | it("keeps Codex's own default when no preferred model is available", () => { |
| 131 | 148 | const models = applyPreferredCodexDefaultModel([ |
| 132 | 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 | 25 | } from "@t3tools/contracts"; |
| 26 | 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 | 33 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; |
| 30 | 34 | import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts"; |
| 31 | 35 | import { |
| ... | ... | @@ -232,9 +236,9 @@ function parseCodexModelListResponse( |
| 232 | 236 | export function applyPreferredCodexDefaultModel( |
| 233 | 237 | models: ReadonlyArray<ServerProviderModel>, |
| 234 | 238 | ): ReadonlyArray<ServerProviderModel> { |
| 235 | const preferredSlug = PREFERRED_DEFAULT_CODEX_MODELS.find((slug) => | |
| 236 | models.some((model) => model.slug === slug && !model.isCustom), | |
| 237 | ); | |
| 239 | const preferredSlug = PREFERRED_DEFAULT_CODEX_MODELS.flatMap((slug) => | |
| 240 | models.filter((model) => !model.isCustom && codexModelFamily(model.slug) === slug), | |
| 241 | )[0]?.slug; | |
| 238 | 242 | if (!preferredSlug) { |
| 239 | 243 | return models; |
| 240 | 244 | } |
apps/server/src/provider/ModelManifest.test.ts+29| ... | ... | @@ -38,6 +38,20 @@ const model = (overrides: Partial<ServerProviderModel>): ServerProviderModel => |
| 38 | 38 | }); |
| 39 | 39 | |
| 40 | 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 | 55 | it("flags non-current models, clears stale flags, and skips custom models", () => { |
| 42 | 56 | const manifest: ModelManifestData = { |
| 43 | 57 | version: 1, |
| ... | ... | @@ -64,6 +78,21 @@ describe("classifyModels", () => { |
| 64 | 78 | }); |
| 65 | 79 | |
| 66 | 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 | 96 | it("moves the default flag and its aliases to the manifest's chat default", () => { |
| 68 | 97 | const driver = ProviderDriverKind.make("antigravity"); |
| 69 | 98 | const manifest: ModelManifestData = { |
apps/server/src/provider/ModelManifest.ts+18-6| ... | ... | @@ -19,6 +19,7 @@ import { |
| 19 | 19 | type ProviderDriverKind, |
| 20 | 20 | type ServerProviderModel, |
| 21 | 21 | } from "@t3tools/contracts"; |
| 22 | import { codexModelFamily } from "@t3tools/shared/model"; | |
| 22 | 23 | import * as Clock from "effect/Clock"; |
| 23 | 24 | import * as Context from "effect/Context"; |
| 24 | 25 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -212,13 +213,15 @@ function isLegacyModel( |
| 212 | 213 | driverKind: ProviderDriverKind, |
| 213 | 214 | slug: string, |
| 214 | 215 | ): boolean { |
| 215 | const catalogModel = manifest.providers?.[driverKind]?.models.find( | |
| 216 | (model) => model.slug === slug, | |
| 217 | ); | |
| 216 | const family = driverKind === "codex" ? codexModelFamily(slug) : slug; | |
| 217 | const catalog = manifest.providers?.[driverKind]?.models; | |
| 218 | const catalogModel = | |
| 219 | catalog?.find((model) => model.slug === slug) ?? | |
| 220 | catalog?.find((model) => model.slug === family); | |
| 218 | 221 | if (catalogModel) return catalogModel.status === "legacy"; |
| 219 | 222 | const currentModels = manifest.currentModels[driverKind]; |
| 220 | 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 | 263 | manifest: ModelManifestData, |
| 261 | 264 | driverKind: ProviderDriverKind, |
| 262 | 265 | ): ReadonlyArray<ServerProviderModel> { |
| 263 | const slug = manifestDefaultModel(manifest, driverKind); | |
| 264 | if (slug === undefined || !models.some((model) => model.slug === slug)) return models; | |
| 266 | const requestedSlug = manifestDefaultModel(manifest, driverKind); | |
| 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 | 277 | const previous = models.find((model) => model.isDefault && model.slug !== slug); |
| 266 | 278 | if (!previous) return models; |
| 267 | 279 | const movedAliases = previous.aliases ?? []; |
apps/server/src/textGeneration/CodexTextGeneration.test.ts+34-1| ... | ... | @@ -134,6 +134,7 @@ function withFakeCodexEnv<A, E, R>( |
| 134 | 134 | input: FakeCodexInput & { |
| 135 | 135 | launchArgs?: string; |
| 136 | 136 | environment?: NodeJS.ProcessEnv; |
| 137 | models?: ReadonlyArray<string>; | |
| 137 | 138 | }, |
| 138 | 139 | effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>, |
| 139 | 140 | ) { |
| ... | ... | @@ -142,12 +143,44 @@ function withFakeCodexEnv<A, E, R>( |
| 142 | 143 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-codex-text-" }); |
| 143 | 144 | const codexPath = yield* makeFakeCodexBinary(tempDir, input); |
| 144 | 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 | 158 | return yield* effectFn(textGeneration); |
| 147 | 159 | }).pipe(Effect.scoped); |
| 148 | 160 | } |
| 149 | 161 | |
| 150 | 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 | 184 | it.effect("generates and sanitizes commit messages without branch by default", () => |
| 152 | 185 | withFakeCodexEnv( |
| 153 | 186 | { |
apps/server/src/textGeneration/CodexTextGeneration.ts+12-2| ... | ... | @@ -11,6 +11,7 @@ import { |
| 11 | 11 | type CodexSettings, |
| 12 | 12 | DEFAULT_TEXT_GENERATION_REASONING_EFFORT, |
| 13 | 13 | type ModelSelection, |
| 14 | type ServerProviderModel, | |
| 14 | 15 | TextGenerationError, |
| 15 | 16 | } from "@t3tools/contracts"; |
| 16 | 17 | import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; |
| ... | ... | @@ -34,7 +35,7 @@ import { |
| 34 | 35 | sanitizeThreadTitle, |
| 35 | 36 | toJsonSchemaObject, |
| 36 | 37 | } from "./TextGenerationUtils.ts"; |
| 37 | import { getModelSelectionStringOptionValue } from "@t3tools/shared/model"; | |
| 38 | import { codexModelFamily, getModelSelectionStringOptionValue } from "@t3tools/shared/model"; | |
| 38 | 39 | import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts"; |
| 39 | 40 | |
| 40 | 41 | const CODEX_TIMEOUT_MS = 180_000; |
| ... | ... | @@ -46,6 +47,7 @@ const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknow |
| 46 | 47 | export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(function* ( |
| 47 | 48 | codexConfig: CodexSettings, |
| 48 | 49 | environment?: NodeJS.ProcessEnv, |
| 50 | getModels: Effect.Effect<ReadonlyArray<ServerProviderModel>> = Effect.succeed([]), | |
| 49 | 51 | ) { |
| 50 | 52 | const fileSystem = yield* FileSystem.FileSystem; |
| 51 | 53 | const path = yield* Path.Path; |
| ... | ... | @@ -178,6 +180,14 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 178 | 180 | const outputPath = yield* writeTempFile(operation, "codex-output", ""); |
| 179 | 181 | |
| 180 | 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 | 191 | const launchArgs = resolveCodexLaunchArgs(codexConfig.launchArgs, resolvedEnvironment); |
| 182 | 192 | const reasoningEffort = |
| 183 | 193 | getModelSelectionStringOptionValue(modelSelection, "reasoningEffort") ?? |
| ... | ... | @@ -193,7 +203,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 193 | 203 | "-s", |
| 194 | 204 | "read-only", |
| 195 | 205 | "--model", |
| 196 | modelSelection.model, | |
| 206 | model, | |
| 197 | 207 | "--config", |
| 198 | 208 | `model_reasoning_effort="${reasoningEffort}"`, |
| 199 | 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 | 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 | 233 | export function normalizeModelSlug( |
| 229 | 234 | model: string | null | undefined, |
| 230 | 235 | provider: ProviderDriverKind = DEFAULT_PROVIDER_DRIVER_KIND, |