fix(ai,coding-agent): narrow Model.compat consumers after AnthropicCompat split
Commit a190397d8 made `Model.compat` resolve to `OpenAICompat | AnthropicCompat`
under the default `TApi = any`. The widened union broke every site that treated
`compat` as openai-shaped: model-registry deep-merge, openai-completions resolved
compat, and ~20 test fixtures. This restores the assumption locally instead of
papering over it with casts.
- getBundledModel is now generic on TApi so test fixtures that spread it into
`Model<"openai-completions">` get the narrow compat back.
- mergeCompat is generic over TBase/TOverride; the schema-driven model-registry
override path keeps its OpenAICompat-shaped merge fields, anthropic overrides
pass through untouched.
- OpenAICompatSchema gains the openai-only fields it was missing
(requiresMistralToolIds, reasoningContentField, requiresReasoningContent*,
thinkingFormat, requiresThinkingAsText, disableReasoningOnForcedToolChoice).
- resolveOpenAICompat fills in disableReasoningOnForcedToolChoice so the
Required<OpenAICompat> shape stays satisfied.
- Anthropic tool-result block id assignment uses the proper unknown double-cast.
- isForcedToolChoice accepts unknown so it can read `params.tool_choice` whose
type comes from the OpenAI SDK ChatCompletionToolChoiceOption (now wider than
our local OpenAICompletionsToolChoice).
- Test fixtures and Required<OpenAICompat> literals updated for the field set.
Fixes CI red on main.
This commit is contained in:
@@ -21,9 +21,9 @@ for (const [provider, models] of Object.entries(MODELS)) {
|
||||
|
||||
export type GeneratedProvider = keyof typeof MODELS;
|
||||
|
||||
export function getBundledModel(provider: GeneratedProvider, modelId: string): Model<Api> {
|
||||
export function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvider, modelId: string): Model<TApi> {
|
||||
const providerModels = modelRegistry.get(provider);
|
||||
return providerModels?.get(modelId) as Model<Api>;
|
||||
return providerModels?.get(modelId) as Model<TApi>;
|
||||
}
|
||||
|
||||
export function getBundledProviders(): KnownProvider[] {
|
||||
|
||||
@@ -1661,7 +1661,7 @@ function buildToolResultBlock(model: Model<"anthropic-messages">, msg: ToolResul
|
||||
};
|
||||
if (isZaiAnthropicEndpoint(model)) {
|
||||
// Z.AI workaround (issue #814): include `id` aliased to `tool_use_id`.
|
||||
(block as Record<string, unknown>).id = msg.toolCallId;
|
||||
(block as unknown as Record<string, unknown>).id = msg.toolCallId;
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
@@ -185,6 +185,8 @@ export function resolveOpenAICompat(
|
||||
model.compat.requiresReasoningContentForToolCalls ?? detected.requiresReasoningContentForToolCalls,
|
||||
requiresAssistantContentForToolCalls:
|
||||
model.compat.requiresAssistantContentForToolCalls ?? detected.requiresAssistantContentForToolCalls,
|
||||
disableReasoningOnForcedToolChoice:
|
||||
model.compat.disableReasoningOnForcedToolChoice ?? detected.disableReasoningOnForcedToolChoice,
|
||||
openRouterRouting: model.compat.openRouterRouting ?? detected.openRouterRouting,
|
||||
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
|
||||
supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode,
|
||||
|
||||
@@ -14,16 +14,16 @@ import type { AssistantMessage, Model, ToolResultMessage, UserMessage } from "@o
|
||||
* endpoints must remain unchanged (no `id` field).
|
||||
*/
|
||||
|
||||
const baseModel = {
|
||||
const baseModel: Omit<Model<"anthropic-messages">, "provider" | "baseUrl"> = {
|
||||
api: "anthropic-messages",
|
||||
id: "glm-4.6",
|
||||
name: "GLM-4.6",
|
||||
input: ["text"] as const,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200000,
|
||||
reasoning: false,
|
||||
} as const;
|
||||
};
|
||||
|
||||
const zaiModel: Model<"anthropic-messages"> = {
|
||||
...baseModel,
|
||||
@@ -66,7 +66,7 @@ const assistant: AssistantMessage = {
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "tool_use",
|
||||
stopReason: "toolUse",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { Effort } from "../src/model-thinking";
|
||||
import { streamAnthropic } from "../src/providers/anthropic";
|
||||
import type { Context, Model, Tool } from "../src/types";
|
||||
|
||||
@@ -79,8 +80,9 @@ describe("issue #826: Anthropic strict-tools opt-out for Vertex-style proxies",
|
||||
reasoning: true,
|
||||
thinking: {
|
||||
mode: "anthropic-adaptive",
|
||||
supportsAdaptiveEffort: true,
|
||||
} as Model<"anthropic-messages">["thinking"],
|
||||
minLevel: Effort.Minimal,
|
||||
maxLevel: Effort.XHigh,
|
||||
},
|
||||
};
|
||||
const { promise, resolve } = Promise.withResolvers<{ thinking?: { type?: string } }>();
|
||||
void streamAnthropic(adaptiveModel, baseContext, {
|
||||
@@ -104,8 +106,9 @@ describe("issue #826: Anthropic strict-tools opt-out for Vertex-style proxies",
|
||||
reasoning: true,
|
||||
thinking: {
|
||||
mode: "anthropic-adaptive",
|
||||
supportsAdaptiveEffort: true,
|
||||
} as Model<"anthropic-messages">["thinking"],
|
||||
minLevel: Effort.Minimal,
|
||||
maxLevel: Effort.XHigh,
|
||||
},
|
||||
compat: { disableAdaptiveThinking: true },
|
||||
};
|
||||
const { promise, resolve } = Promise.withResolvers<{ thinking?: { type?: string; budget_tokens?: number } }>();
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
||||
import { DEFAULT_MODEL_PER_PROVIDER, PROVIDER_DESCRIPTORS } from "../src/provider-models/descriptors";
|
||||
import { MODELS_DEV_PROVIDER_DESCRIPTORS } from "../src/provider-models/openai-compat";
|
||||
import { getEnvApiKey } from "../src/stream";
|
||||
import type { OpenAICompat } from "../src/types";
|
||||
|
||||
describe("deepseek built-in provider (issue #830)", () => {
|
||||
test("registers built-in runtime descriptor with DEEPSEEK_API_KEY env discovery", () => {
|
||||
@@ -35,10 +36,12 @@ describe("deepseek built-in provider (issue #830)", () => {
|
||||
// Per-model compat: deepseek-v4 reasoning models leak chat-template tool-call markers
|
||||
// (#798) and 400 on tool_choice when xhigh effort is used (#830 thread). Reasoning content
|
||||
// must round-trip on tool calls (interleaved.field=reasoning_content from models.dev).
|
||||
expect(descriptor?.compat?.supportsReasoningEffort).toBe(true);
|
||||
expect(descriptor?.compat?.supportsToolChoice).toBe(false);
|
||||
expect(descriptor?.compat?.requiresReasoningContentForToolCalls).toBe(true);
|
||||
expect(descriptor?.compat?.reasoningContentField).toBe("reasoning_content");
|
||||
expect(descriptor?.compat?.reasoningEffortMap?.xhigh).toBe("max");
|
||||
const compat =
|
||||
descriptor?.api === "openai-completions" ? (descriptor.compat as OpenAICompat | undefined) : undefined;
|
||||
expect(compat?.supportsReasoningEffort).toBe(true);
|
||||
expect(compat?.supportsToolChoice).toBe(false);
|
||||
expect(compat?.requiresReasoningContentForToolCalls).toBe(true);
|
||||
expect(compat?.reasoningContentField).toBe("reasoning_content");
|
||||
expect(compat?.reasoningEffortMap?.xhigh).toBe("max");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,6 +73,7 @@ describe("openai-completions compatibility", () => {
|
||||
reasoningEffortMap: {},
|
||||
supportsUsageInStreaming: true,
|
||||
supportsToolChoice: true,
|
||||
disableReasoningOnForcedToolChoice: false,
|
||||
maxTokensField: "max_completion_tokens",
|
||||
requiresToolResultName: false,
|
||||
requiresAssistantAfterToolResult: false,
|
||||
|
||||
@@ -19,6 +19,7 @@ const compat: Required<OpenAICompat> = {
|
||||
reasoningEffortMap: {},
|
||||
supportsUsageInStreaming: true,
|
||||
supportsToolChoice: true,
|
||||
disableReasoningOnForcedToolChoice: false,
|
||||
maxTokensField: "max_completion_tokens",
|
||||
requiresToolResultName: false,
|
||||
requiresAssistantAfterToolResult: false,
|
||||
@@ -51,7 +52,7 @@ function buildToolResult(toolCallId: string, timestamp: number): ToolResultMessa
|
||||
|
||||
describe("openai-completions convertMessages", () => {
|
||||
it("batches tool-result images after consecutive tool results", () => {
|
||||
const baseModel = getBundledModel("openai", "gpt-4o-mini");
|
||||
const baseModel = getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">;
|
||||
const model: Model<"openai-completions"> = {
|
||||
...baseModel,
|
||||
api: "openai-completions",
|
||||
@@ -94,7 +95,7 @@ describe("openai-completions convertMessages", () => {
|
||||
expect(imageParts.length).toBe(2);
|
||||
});
|
||||
it("uses generated tool_call_id values when assistant/tool IDs are empty", () => {
|
||||
const baseModel = getBundledModel("openai", "gpt-4o-mini");
|
||||
const baseModel = getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">;
|
||||
const model: Model<"openai-completions"> = {
|
||||
...baseModel,
|
||||
api: "openai-completions",
|
||||
@@ -143,7 +144,7 @@ describe("openai-completions convertMessages", () => {
|
||||
});
|
||||
|
||||
it("serializes string tool arguments into valid JSON objects", () => {
|
||||
const baseModel = getBundledModel("openai", "gpt-4o-mini");
|
||||
const baseModel = getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">;
|
||||
const model: Model<"openai-completions"> = {
|
||||
...baseModel,
|
||||
api: "openai-completions",
|
||||
|
||||
@@ -10,7 +10,7 @@ const originalFetch = global.fetch;
|
||||
|
||||
const openAIResponsesModel = getBundledModel("openai", "gpt-5-mini") as Model<"openai-responses">;
|
||||
const openAICompletionsModel = {
|
||||
...getBundledModel("openai", "gpt-4o-mini"),
|
||||
...(getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">),
|
||||
api: "openai-completions",
|
||||
} satisfies Model<"openai-completions">;
|
||||
const azureOpenAIResponsesModel: Model<"azure-openai-responses"> = {
|
||||
|
||||
@@ -71,7 +71,7 @@ function captureResponsesPayload(model: Model<"openai-responses">): Promise<unkn
|
||||
describe("OpenAI tool strict mode", () => {
|
||||
it("sends strict=true for openai-completions tool schemas", async () => {
|
||||
const model: Model<"openai-completions"> = {
|
||||
...getBundledModel("openai", "gpt-4o-mini"),
|
||||
...(getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">),
|
||||
api: "openai-completions",
|
||||
};
|
||||
|
||||
@@ -83,7 +83,7 @@ describe("OpenAI tool strict mode", () => {
|
||||
|
||||
it("omits strict for openai-completions when compatibility disables strict mode", async () => {
|
||||
const model: Model<"openai-completions"> = {
|
||||
...getBundledModel("openai", "gpt-4o-mini"),
|
||||
...(getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">),
|
||||
api: "openai-completions",
|
||||
compat: { supportsStrictMode: false } satisfies OpenAICompat,
|
||||
};
|
||||
@@ -123,7 +123,7 @@ describe("OpenAI tool strict mode", () => {
|
||||
|
||||
it("uses uniformly non-strict tool schemas when provider requires all-or-none strictness", async () => {
|
||||
const model: Model<"openai-completions"> = {
|
||||
...getBundledModel("openai", "gpt-4o-mini"),
|
||||
...(getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">),
|
||||
api: "openai-completions",
|
||||
compat: { toolStrictMode: "all_strict" } satisfies OpenAICompat,
|
||||
};
|
||||
@@ -150,7 +150,7 @@ describe("OpenAI tool strict mode", () => {
|
||||
|
||||
it("surfaces captured JSON error bodies when the SDK reports no body", async () => {
|
||||
const model: Model<"openai-completions"> = {
|
||||
...getBundledModel("openai", "gpt-4o-mini"),
|
||||
...(getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">),
|
||||
api: "openai-completions",
|
||||
};
|
||||
global.fetch = Object.assign(
|
||||
@@ -179,7 +179,7 @@ describe("OpenAI tool strict mode", () => {
|
||||
|
||||
it("retries with non-strict tool schemas after strict-mode request errors", async () => {
|
||||
const model: Model<"openai-completions"> = {
|
||||
...getBundledModel("openai", "gpt-4o-mini"),
|
||||
...(getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">),
|
||||
api: "openai-completions",
|
||||
compat: { toolStrictMode: "all_strict" } satisfies OpenAICompat,
|
||||
};
|
||||
|
||||
@@ -71,7 +71,7 @@ function createSseResponse(events: unknown[], headers: Record<string, string> =
|
||||
describe("streamSimple onResponse propagation", () => {
|
||||
it("invokes onResponse for the default openai-completions path through streamSimple", async () => {
|
||||
const model: Model<"openai-completions"> = {
|
||||
...getBundledModel("openai", "gpt-4o-mini"),
|
||||
...(getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">),
|
||||
api: "openai-completions",
|
||||
};
|
||||
|
||||
|
||||
@@ -588,7 +588,7 @@ describe("Generate E2E Tests", () => {
|
||||
|
||||
describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("OpenAI Completions Provider (gpt-4o-mini)", () => {
|
||||
const llm: Model<"openai-completions"> = {
|
||||
...getBundledModel("openai", "gpt-4o-mini"),
|
||||
...(getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">),
|
||||
api: "openai-completions",
|
||||
};
|
||||
|
||||
@@ -626,7 +626,7 @@ describe("Generate E2E Tests", () => {
|
||||
});
|
||||
|
||||
describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("OpenAI Responses Provider (gpt-5-mini)", () => {
|
||||
const llm = getBundledModel("openai", "gpt-5-mini");
|
||||
const llm = getBundledModel("openai", "gpt-5-mini") as Model<"openai-responses">;
|
||||
|
||||
it(
|
||||
"should complete basic text generation",
|
||||
@@ -714,7 +714,7 @@ describe("Generate E2E Tests", () => {
|
||||
});
|
||||
|
||||
describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("OpenAI Responses Provider (gpt-5-mini)", () => {
|
||||
const model = getBundledModel("openai", "gpt-5-mini");
|
||||
const model = getBundledModel("openai", "gpt-5-mini") as Model<"openai-responses">;
|
||||
|
||||
it(
|
||||
"should complete basic text generation",
|
||||
|
||||
@@ -83,7 +83,7 @@ describe("Token Statistics on Abort", () => {
|
||||
|
||||
describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("OpenAI Completions Provider", () => {
|
||||
const llm: Model<"openai-completions"> = {
|
||||
...getBundledModel("openai", "gpt-4o-mini")!,
|
||||
...(getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">)!,
|
||||
api: "openai-completions",
|
||||
};
|
||||
|
||||
@@ -97,7 +97,7 @@ describe("Token Statistics on Abort", () => {
|
||||
});
|
||||
|
||||
describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("OpenAI Responses Provider", () => {
|
||||
const llm = getBundledModel("openai", "gpt-5-mini");
|
||||
const llm = getBundledModel("openai", "gpt-5-mini") as Model<"openai-responses">;
|
||||
|
||||
it(
|
||||
"should include token stats when aborted mid-stream",
|
||||
|
||||
@@ -109,7 +109,7 @@ describe("Tool Call Without Result Tests", () => {
|
||||
|
||||
describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("OpenAI Completions Provider", () => {
|
||||
const model: Model<"openai-completions"> = {
|
||||
...getBundledModel("openai", "gpt-4o-mini")!,
|
||||
...(getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">)!,
|
||||
api: "openai-completions",
|
||||
};
|
||||
|
||||
@@ -123,7 +123,7 @@ describe("Tool Call Without Result Tests", () => {
|
||||
});
|
||||
|
||||
describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("OpenAI Responses Provider", () => {
|
||||
const model = getBundledModel("openai", "gpt-5-mini");
|
||||
const model = getBundledModel("openai", "gpt-5-mini") as Model<"openai-responses">;
|
||||
|
||||
it(
|
||||
"should filter out tool calls without corresponding tool results",
|
||||
|
||||
@@ -154,7 +154,7 @@ describe("totalTokens field", () => {
|
||||
"gpt-4o-mini - should return totalTokens equal to sum of components",
|
||||
async () => {
|
||||
const llm: Model<"openai-completions"> = {
|
||||
...getBundledModel("openai", "gpt-4o-mini")!,
|
||||
...(getBundledModel("openai", "gpt-4o-mini") as Model<"openai-completions">)!,
|
||||
api: "openai-completions",
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("xhigh reasoning", () => {
|
||||
|
||||
describe("gpt-5-mini (does not support xhigh)", () => {
|
||||
it("should error with openai-responses when using xhigh", async () => {
|
||||
const model = getBundledModel("openai", "gpt-5-mini");
|
||||
const model = getBundledModel("openai", "gpt-5-mini") as Model<"openai-responses">;
|
||||
const s = stream(model, makeContext(), { reasoning: "xhigh" });
|
||||
|
||||
for await (const _ of s) {
|
||||
@@ -53,7 +53,7 @@ describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("xhigh reasoning", () => {
|
||||
|
||||
it("should error with openai-completions when using xhigh", async () => {
|
||||
const model: Model<"openai-completions"> = {
|
||||
...getBundledModel("openai", "gpt-5-mini"),
|
||||
...(getBundledModel("openai", "gpt-5-mini") as Model<"openai-completions">),
|
||||
api: "openai-completions",
|
||||
};
|
||||
const s = stream(model, makeContext(), { reasoning: "xhigh" });
|
||||
|
||||
@@ -145,8 +145,16 @@ const OpenAICompatSchema = Type.Object({
|
||||
maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
|
||||
supportsUsageInStreaming: Type.Optional(Type.Boolean()),
|
||||
requiresToolResultName: Type.Optional(Type.Boolean()),
|
||||
requiresMistralToolIds: Type.Optional(Type.Boolean()),
|
||||
requiresAssistantAfterToolResult: Type.Optional(Type.Boolean()),
|
||||
requiresThinkingAsText: Type.Optional(Type.Boolean()),
|
||||
reasoningContentField: Type.Optional(
|
||||
Type.Union([Type.Literal("reasoning_content"), Type.Literal("reasoning"), Type.Literal("reasoning_text")]),
|
||||
),
|
||||
requiresReasoningContentForToolCalls: Type.Optional(Type.Boolean()),
|
||||
requiresAssistantContentForToolCalls: Type.Optional(Type.Boolean()),
|
||||
supportsToolChoice: Type.Optional(Type.Boolean()),
|
||||
disableReasoningOnForcedToolChoice: Type.Optional(Type.Boolean()),
|
||||
thinkingFormat: Type.Optional(
|
||||
Type.Union([
|
||||
Type.Literal("openai"),
|
||||
@@ -559,27 +567,20 @@ function resolveOAuthAccountIdForAccessToken(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function mergeCompat(
|
||||
baseCompat: Model<Api>["compat"],
|
||||
overrideCompat: ModelOverride["compat"],
|
||||
): Model<Api>["compat"] | undefined {
|
||||
function mergeCompat<TBase extends object, TOverride extends object>(
|
||||
baseCompat: TBase | null | undefined,
|
||||
overrideCompat: TOverride | null | undefined,
|
||||
): (TBase & TOverride) | TBase | TOverride | undefined {
|
||||
if (!baseCompat) return overrideCompat ?? undefined;
|
||||
if (!overrideCompat) return baseCompat;
|
||||
const base = baseCompat ?? {};
|
||||
const override = overrideCompat;
|
||||
const merged: NonNullable<Model<Api>["compat"]> = { ...base, ...override };
|
||||
if (baseCompat?.reasoningEffortMap || overrideCompat.reasoningEffortMap) {
|
||||
merged.reasoningEffortMap = { ...baseCompat?.reasoningEffortMap, ...overrideCompat.reasoningEffortMap };
|
||||
|
||||
const merged: Record<string, unknown> = { ...(baseCompat as Record<string, unknown>) };
|
||||
for (const [key, overrideValue] of Object.entries(overrideCompat)) {
|
||||
const baseValue = (baseCompat as Record<string, unknown>)[key];
|
||||
merged[key] =
|
||||
isRecord(baseValue) && isRecord(overrideValue) ? mergeCompat(baseValue, overrideValue) : overrideValue;
|
||||
}
|
||||
if (baseCompat?.openRouterRouting || overrideCompat.openRouterRouting) {
|
||||
merged.openRouterRouting = { ...baseCompat?.openRouterRouting, ...overrideCompat.openRouterRouting };
|
||||
}
|
||||
if (baseCompat?.vercelGatewayRouting || overrideCompat.vercelGatewayRouting) {
|
||||
merged.vercelGatewayRouting = { ...baseCompat?.vercelGatewayRouting, ...overrideCompat.vercelGatewayRouting };
|
||||
}
|
||||
if (baseCompat?.extraBody || overrideCompat.extraBody) {
|
||||
merged.extraBody = { ...baseCompat?.extraBody, ...overrideCompat.extraBody };
|
||||
}
|
||||
return merged;
|
||||
return merged as TBase & TOverride;
|
||||
}
|
||||
|
||||
function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {
|
||||
|
||||
@@ -67,6 +67,7 @@ describe("issue #816 — plan mode pendingModelSwitch leak", () => {
|
||||
model: planModel,
|
||||
thinkingLevel: undefined,
|
||||
explicitThinkingLevel: false,
|
||||
warning: undefined,
|
||||
});
|
||||
// Avoid kicking off real session work during plan mode entry.
|
||||
vi.spyOn(session, "sendPlanModeContext").mockResolvedValue(undefined);
|
||||
|
||||
@@ -93,6 +93,10 @@ describe("ModelRegistry", () => {
|
||||
return registry.getAll().filter(m => m.provider === provider);
|
||||
}
|
||||
|
||||
function getOpenAICompat(model: Model | undefined): OpenAICompat | undefined {
|
||||
return model?.api === "openai-completions" ? (model.compat as OpenAICompat | undefined) : undefined;
|
||||
}
|
||||
|
||||
/** Create a baseUrl-only override (no custom models) */
|
||||
function overrideConfig(baseUrl: string, headers?: Record<string, string>) {
|
||||
return { baseUrl, ...(headers && { headers }) };
|
||||
@@ -511,8 +515,8 @@ describe("ModelRegistry", () => {
|
||||
const models = getModelsForProvider(registry, "openrouter");
|
||||
expect(models.length).toBeGreaterThan(0);
|
||||
for (const model of models) {
|
||||
expect(model.compat?.supportsUsageInStreaming).toBe(false);
|
||||
expect(model.compat?.supportsStrictMode).toBe(false);
|
||||
expect(getOpenAICompat(model)?.supportsUsageInStreaming).toBe(false);
|
||||
expect(getOpenAICompat(model)?.supportsStrictMode).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -541,8 +545,9 @@ describe("ModelRegistry", () => {
|
||||
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
const model = registry.find("demo", "demo-model");
|
||||
expect(model?.compat?.supportsUsageInStreaming).toBe(false);
|
||||
expect(model?.compat?.maxTokensField).toBe("max_tokens");
|
||||
const compat = getOpenAICompat(model);
|
||||
expect(compat?.supportsUsageInStreaming).toBe(false);
|
||||
expect(compat?.maxTokensField).toBe("max_tokens");
|
||||
});
|
||||
|
||||
test("model-level compat overrides provider-level compat for custom models", () => {
|
||||
@@ -574,8 +579,9 @@ describe("ModelRegistry", () => {
|
||||
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
const model = registry.find("demo", "demo-model");
|
||||
expect(model?.compat?.supportsUsageInStreaming).toBe(true);
|
||||
expect(model?.compat?.maxTokensField).toBe("max_completion_tokens");
|
||||
const compat = getOpenAICompat(model);
|
||||
expect(compat?.supportsUsageInStreaming).toBe(true);
|
||||
expect(compat?.maxTokensField).toBe("max_completion_tokens");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -883,12 +889,12 @@ describe("ModelRegistry", () => {
|
||||
},
|
||||
});
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
expect(registry.find("openai", "gpt-5.4")?.compat?.extraBody).toEqual({ source: "proxy" });
|
||||
expect(getOpenAICompat(registry.find("openai", "gpt-5.4"))?.extraBody).toEqual({ source: "proxy" });
|
||||
|
||||
using _hook = mockOpenAiCompatibleModels("https://my-proxy.example.com/v1/models", ["gpt-5.4"]);
|
||||
await registry.refreshProvider("openai", "online");
|
||||
|
||||
expect(registry.find("openai", "gpt-5.4")?.compat?.extraBody).toEqual({ source: "proxy" });
|
||||
expect(getOpenAICompat(registry.find("openai", "gpt-5.4"))?.extraBody).toEqual({ source: "proxy" });
|
||||
});
|
||||
|
||||
test("modelOverrides still apply after discoverable refresh", async () => {
|
||||
@@ -962,9 +968,10 @@ describe("ModelRegistry", () => {
|
||||
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
const model = registry.find("minimax-code", "MiniMax-M2.5");
|
||||
expect(model?.compat?.thinkingFormat).toBeUndefined();
|
||||
expect(model?.compat?.reasoningContentField).toBeUndefined();
|
||||
expect(model?.compat?.extraBody).toEqual({ source: "proxy" });
|
||||
const compat = getOpenAICompat(model);
|
||||
expect(compat?.thinkingFormat).toBeUndefined();
|
||||
expect(compat?.reasoningContentField).toBeUndefined();
|
||||
expect(compat?.extraBody).toEqual({ source: "proxy" });
|
||||
});
|
||||
|
||||
test("removing custom models from models.json keeps built-in provider models", async () => {
|
||||
|
||||
Reference in New Issue
Block a user