Skip to content

Commit 7829bf1

Browse files
ammar-agentroot
andauthored
🤖 refactor: centralize model constants + fix previousResponseId bug (#584)
## Summary Centralizes all model metadata into `knownModels.ts` and fixes a bug where `previousResponseId` was incorrectly passed between different models or contexts. ## Changes ### 1. Centralized Model Constants (`knownModels.ts`) - **Fixed `GPT_MINI` model ID**: Changed to `gpt-5.1-codex-mini` (correct LiteLLM name) - **Eliminated duplication**: ID strings auto-constructed from `provider` + `providerModelId` - **Removed string constant exports**: Callers now use `KNOWN_MODELS.SONNET.id` directly - **Programmatic `MODEL_NAMES`**: Auto-groups models by provider via `.reduce()` - **Type-safe keys**: `KnownModelKey` derived from object keys, never out of sync - **Integration test**: Verifies all models exist in `models.json` ### 2. Fixed `previousResponseId` Bug **Problem**: We were passing `previousResponseId` from any previous OpenAI message, causing: - `APICallError: Previous response with id 'resp_...' not found` when switching models - Invalid response IDs for non-reasoning models - Expired response IDs **Solution**: Now only pass `previousResponseId` when: 1. Current model uses reasoning (`reasoningEffort` is set) 2. Previous message was from the **same model** 3. Stop searching if we find a different model (context changed) ### 3. Test Improvements - Updated `PROVIDER_CONFIGS` to use `KNOWN_MODELS` constants - Added multi-turn integration test for CODEX model - Verifies `responseId` persistence across conversation turns ## Benefits - **Single source of truth**: Only edit one place to update model versions - **No manual maintenance**: All derived collections auto-update - **Regression prevention**: Integration test catches model data issues - **Bug fix**: Prevents APICallError when using reasoning models ## Testing ```bash make typecheck # ✅ All checks pass TEST_INTEGRATION=1 bun x jest tests/models/knownModels.test.ts # ✅ 2/2 passed ``` _Generated with `mux`_ --------- Co-authored-by: root <root@ovh-1.tailc2a514.ts.net>
1 parent 27b6a10 commit 7829bf1

25 files changed

+654
-107
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* Centralized model metadata. Update model versions here and everywhere else will follow.
3+
*/
4+
5+
type ModelProvider = "anthropic" | "openai";
6+
7+
interface KnownModelDefinition {
8+
/** Provider identifier used by SDK factories */
9+
provider: ModelProvider;
10+
/** Provider-specific model name (no provider prefix) */
11+
providerModelId: string;
12+
/** Aliases that should resolve to this model */
13+
aliases?: string[];
14+
/** Preload tokenizer encodings at startup */
15+
warm?: boolean;
16+
/** Use as global default model */
17+
isDefault?: boolean;
18+
/** Optional tokenizer override for ai-tokenizer */
19+
tokenizerOverride?: string;
20+
}
21+
22+
interface KnownModel extends KnownModelDefinition {
23+
/** Full model id string in the format provider:model */
24+
id: `${ModelProvider}:${string}`;
25+
}
26+
27+
// Model definitions. Note we avoid listing legacy models here. These represent the focal models
28+
// of the community.
29+
const MODEL_DEFINITIONS = {
30+
SONNET: {
31+
provider: "anthropic",
32+
providerModelId: "claude-sonnet-4-5",
33+
aliases: ["sonnet"],
34+
warm: true,
35+
isDefault: true,
36+
tokenizerOverride: "anthropic/claude-sonnet-4.5",
37+
},
38+
HAIKU: {
39+
provider: "anthropic",
40+
providerModelId: "claude-haiku-4-5",
41+
aliases: ["haiku"],
42+
tokenizerOverride: "anthropic/claude-3.5-haiku",
43+
},
44+
OPUS: {
45+
provider: "anthropic",
46+
providerModelId: "claude-opus-4-1",
47+
aliases: ["opus"],
48+
},
49+
GPT: {
50+
provider: "openai",
51+
providerModelId: "gpt-5.1",
52+
aliases: ["gpt-5.1"],
53+
warm: true,
54+
},
55+
GPT_PRO: {
56+
provider: "openai",
57+
providerModelId: "gpt-5-pro",
58+
aliases: ["gpt-5-pro"],
59+
},
60+
GPT_CODEX: {
61+
provider: "openai",
62+
providerModelId: "gpt-5.1-codex",
63+
aliases: ["codex"],
64+
warm: true,
65+
},
66+
GPT_MINI: {
67+
provider: "openai",
68+
providerModelId: "gpt-5.1-codex-mini",
69+
},
70+
} as const satisfies Record<string, KnownModelDefinition>;
71+
72+
export type KnownModelKey = keyof typeof MODEL_DEFINITIONS;
73+
74+
export const KNOWN_MODELS = Object.fromEntries(
75+
Object.entries(MODEL_DEFINITIONS).map(([key, definition]) => [
76+
key,
77+
{
78+
...definition,
79+
id: `${definition.provider}:${definition.providerModelId}` as `${ModelProvider}:${string}`,
80+
},
81+
])
82+
) as Record<KnownModelKey, KnownModel>;
83+
84+
export function getKnownModel(key: KnownModelKey): KnownModel {
85+
return KNOWN_MODELS[key];
86+
}
87+
88+
// ------------------------------------------------------------------------------------
89+
// Derived collections
90+
// ------------------------------------------------------------------------------------
91+
92+
const DEFAULT_MODEL_ENTRY =
93+
Object.values(KNOWN_MODELS).find((model) => model.isDefault) ?? KNOWN_MODELS.SONNET;
94+
95+
export const DEFAULT_MODEL = DEFAULT_MODEL_ENTRY.id;
96+
97+
export const DEFAULT_WARM_MODELS = Object.values(KNOWN_MODELS)
98+
.filter((model) => model.warm)
99+
.map((model) => model.id);
100+
101+
export const MODEL_ABBREVIATIONS: Record<string, string> = Object.fromEntries(
102+
Object.values(KNOWN_MODELS)
103+
.flatMap((model) => (model.aliases ?? []).map((alias) => [alias, model.id] as const))
104+
.sort(([a], [b]) => a.localeCompare(b))
105+
);
106+
107+
export const TOKENIZER_MODEL_OVERRIDES: Record<string, string> = Object.fromEntries(
108+
Object.values(KNOWN_MODELS)
109+
.filter((model) => Boolean(model.tokenizerOverride))
110+
.map((model) => [model.id, model.tokenizerOverride!])
111+
);
112+
113+
export const MODEL_NAMES: Record<ModelProvider, Record<string, string>> = Object.entries(
114+
KNOWN_MODELS
115+
).reduce<Record<ModelProvider, Record<string, string>>>(
116+
(acc, [key, model]) => {
117+
if (!acc[model.provider]) {
118+
const emptyRecord: Record<string, string> = {};
119+
acc[model.provider] = emptyRecord;
120+
}
121+
acc[model.provider][key] = model.providerModelId;
122+
return acc;
123+
},
124+
{} as Record<ModelProvider, Record<string, string>>
125+
);

‎src/hooks/useModelLRU.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ const MAX_LRU_SIZE = 8;
77
const LRU_KEY = "model-lru";
88

99
// Default models from abbreviations (for initial LRU population)
10-
const DEFAULT_MODELS = Object.values(MODEL_ABBREVIATIONS);
10+
// Ensure defaultModel is first, then fill with other abbreviations
11+
const DEFAULT_MODELS = [
12+
defaultModel,
13+
...Object.values(MODEL_ABBREVIATIONS).filter((m) => m !== defaultModel),
14+
].slice(0, MAX_LRU_SIZE);
1115

1216
/**
1317
* Get the default model from LRU (non-hook version for use outside React)

‎src/services/historyService.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Config } from "@/config";
77
import { workspaceFileLocks } from "@/utils/concurrency/workspaceFileLocks";
88
import { log } from "./log";
99
import { getTokenizerForModel } from "@/utils/main/tokenizer";
10+
import { KNOWN_MODELS } from "@/constants/knownModels";
1011

1112
/**
1213
* HistoryService - Manages chat history persistence and sequence numbering
@@ -340,7 +341,7 @@ export class HistoryService {
340341
}
341342

342343
// Get tokenizer for counting (use a default model)
343-
const tokenizer = await getTokenizerForModel("anthropic:claude-sonnet-4-5");
344+
const tokenizer = await getTokenizerForModel(KNOWN_MODELS.SONNET.id);
344345

345346
// Count tokens for each message
346347
// We stringify the entire message for simplicity - only relative weights matter

‎src/services/mock/mockScenarioPlayer.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import type { StreamStartEvent, StreamDeltaEvent, StreamEndEvent } from "@/types
1818
import type { ToolCallStartEvent, ToolCallEndEvent } from "@/types/stream";
1919
import type { ReasoningDeltaEvent } from "@/types/stream";
2020
import { getTokenizerForModel } from "@/utils/main/tokenizer";
21+
import { KNOWN_MODELS } from "@/constants/knownModels";
2122

22-
const MOCK_TOKENIZER_MODEL = "openai:gpt-5";
23+
const MOCK_TOKENIZER_MODEL = KNOWN_MODELS.GPT.id;
2324
const TOKENIZE_TIMEOUT_MS = 150;
2425
let tokenizerFallbackLogged = false;
2526

‎src/services/mock/scenarios/basicChat.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ScenarioTurn } from "../scenarioTypes";
22
import { STREAM_BASE_DELAY } from "../scenarioTypes";
3+
import { KNOWN_MODELS } from "@/constants/knownModels";
34

45
export const LIST_PROGRAMMING_LANGUAGES = "List 3 programming languages";
56

@@ -12,7 +13,7 @@ const listProgrammingLanguagesTurn: ScenarioTurn = {
1213
assistant: {
1314
messageId: "msg-basic-1",
1415
events: [
15-
{ kind: "stream-start", delay: 0, messageId: "msg-basic-1", model: "openai:gpt-5" },
16+
{ kind: "stream-start", delay: 0, messageId: "msg-basic-1", model: KNOWN_MODELS.GPT.id },
1617
{
1718
kind: "stream-delta",
1819
delay: STREAM_BASE_DELAY,
@@ -37,7 +38,7 @@ const listProgrammingLanguagesTurn: ScenarioTurn = {
3738
kind: "stream-end",
3839
delay: STREAM_BASE_DELAY * 5,
3940
metadata: {
40-
model: "openai:gpt-5",
41+
model: KNOWN_MODELS.GPT.id,
4142
inputTokens: 64,
4243
outputTokens: 48,
4344
systemMessageTokens: 12,

‎src/services/mock/scenarios/permissionModes.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ScenarioTurn } from "../scenarioTypes";
2+
import { KNOWN_MODELS } from "@/constants/knownModels";
23
import { STREAM_BASE_DELAY } from "../scenarioTypes";
34

45
export const PERMISSION_MODE_PROMPTS = {
@@ -19,7 +20,7 @@ const planRefactorTurn: ScenarioTurn = {
1920
kind: "stream-start",
2021
delay: 0,
2122
messageId: "msg-plan-refactor",
22-
model: "openai:gpt-5",
23+
model: KNOWN_MODELS.GPT.id,
2324
},
2425
{
2526
kind: "stream-delta",
@@ -45,7 +46,7 @@ const planRefactorTurn: ScenarioTurn = {
4546
kind: "stream-end",
4647
delay: STREAM_BASE_DELAY * 5,
4748
metadata: {
48-
model: "openai:gpt-5",
49+
model: KNOWN_MODELS.GPT.id,
4950
inputTokens: 180,
5051
outputTokens: 130,
5152
systemMessageTokens: 24,
@@ -74,7 +75,7 @@ const executePlanTurn: ScenarioTurn = {
7475
kind: "stream-start",
7576
delay: 0,
7677
messageId: "msg-exec-refactor",
77-
model: "openai:gpt-5",
78+
model: KNOWN_MODELS.GPT.id,
7879
},
7980
{
8081
kind: "tool-start",
@@ -118,7 +119,7 @@ const executePlanTurn: ScenarioTurn = {
118119
kind: "stream-end",
119120
delay: STREAM_BASE_DELAY * 3,
120121
metadata: {
121-
model: "openai:gpt-5",
122+
model: KNOWN_MODELS.GPT.id,
122123
inputTokens: 220,
123124
outputTokens: 110,
124125
systemMessageTokens: 18,

‎src/services/mock/scenarios/review.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ScenarioTurn } from "../scenarioTypes";
2+
import { KNOWN_MODELS } from "@/constants/knownModels";
23
import { STREAM_BASE_DELAY } from "../scenarioTypes";
34

45
export const REVIEW_PROMPTS = {
@@ -16,7 +17,7 @@ const summarizeBranchesTurn: ScenarioTurn = {
1617
assistant: {
1718
messageId: "msg-plan-1",
1819
events: [
19-
{ kind: "stream-start", delay: 0, messageId: "msg-plan-1", model: "openai:gpt-5" },
20+
{ kind: "stream-start", delay: 0, messageId: "msg-plan-1", model: KNOWN_MODELS.GPT.id },
2021
{
2122
kind: "reasoning-delta",
2223
delay: STREAM_BASE_DELAY,
@@ -61,7 +62,7 @@ const summarizeBranchesTurn: ScenarioTurn = {
6162
kind: "stream-end",
6263
delay: STREAM_BASE_DELAY * 6,
6364
metadata: {
64-
model: "openai:gpt-5",
65+
model: KNOWN_MODELS.GPT.id,
6566
inputTokens: 128,
6667
outputTokens: 85,
6768
systemMessageTokens: 32,
@@ -86,7 +87,7 @@ const openOnboardingDocTurn: ScenarioTurn = {
8687
assistant: {
8788
messageId: "msg-exec-1",
8889
events: [
89-
{ kind: "stream-start", delay: 0, messageId: "msg-exec-1", model: "openai:gpt-5" },
90+
{ kind: "stream-start", delay: 0, messageId: "msg-exec-1", model: KNOWN_MODELS.GPT.id },
9091
{
9192
kind: "tool-start",
9293
delay: STREAM_BASE_DELAY,
@@ -114,7 +115,7 @@ const showOnboardingDocTurn: ScenarioTurn = {
114115
assistant: {
115116
messageId: "msg-exec-2",
116117
events: [
117-
{ kind: "stream-start", delay: 0, messageId: "msg-exec-2", model: "openai:gpt-5" },
118+
{ kind: "stream-start", delay: 0, messageId: "msg-exec-2", model: KNOWN_MODELS.GPT.id },
118119
{
119120
kind: "tool-start",
120121
delay: STREAM_BASE_DELAY,
@@ -153,7 +154,7 @@ const showOnboardingDocTurn: ScenarioTurn = {
153154
kind: "stream-end",
154155
delay: STREAM_BASE_DELAY * 3,
155156
metadata: {
156-
model: "openai:gpt-5",
157+
model: KNOWN_MODELS.GPT.id,
157158
inputTokens: 96,
158159
outputTokens: 142,
159160
systemMessageTokens: 32,

‎src/services/mock/scenarios/slashCommands.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ScenarioTurn } from "../scenarioTypes";
2+
import { KNOWN_MODELS } from "@/constants/knownModels";
23
import { STREAM_BASE_DELAY } from "../scenarioTypes";
34

45
export const SLASH_COMMAND_PROMPTS = {
@@ -24,7 +25,7 @@ const compactConversationTurn: ScenarioTurn = {
2425
kind: "stream-start",
2526
delay: 0,
2627
messageId: "msg-slash-compact-1",
27-
model: "openai:gpt-5",
28+
model: KNOWN_MODELS.GPT.id,
2829
},
2930
{
3031
kind: "stream-delta",
@@ -35,7 +36,7 @@ const compactConversationTurn: ScenarioTurn = {
3536
kind: "stream-end",
3637
delay: STREAM_BASE_DELAY * 2,
3738
metadata: {
38-
model: "openai:gpt-5",
39+
model: KNOWN_MODELS.GPT.id,
3940
inputTokens: 220,
4041
outputTokens: 96,
4142
systemMessageTokens: 18,

0 commit comments

Comments
 (0)