mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 07:10:24 +00:00
Refactor release hardening follow-ups (#39959)
* build: fail fast on stale host-env swift policy * build: sync generated host env swift policy * build: guard bundled extension root dependency gaps * refactor: centralize provider capability quirks * test: table-drive provider regression coverage * fix: block merge when prep branch has unpushed commits * refactor: simplify models config merge preservation
This commit is contained in:
committed by
GitHub
parent
27558806b5
commit
eba9dcc67a
@@ -246,6 +246,21 @@ describe("models-config", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("replaces stale merged baseUrl when the provider api changes", async () => {
|
||||
await withTempHome(async () => {
|
||||
const parsed = await runCustomProviderMergeTest({
|
||||
seedProvider: {
|
||||
baseUrl: "https://agent.example/v1",
|
||||
apiKey: "AGENT_KEY", // pragma: allowlist secret
|
||||
api: "openai-completions",
|
||||
models: [{ id: "agent-model", name: "Agent model", input: ["text"] }],
|
||||
},
|
||||
});
|
||||
expect(parsed.providers.custom?.apiKey).toBe("AGENT_KEY");
|
||||
expect(parsed.providers.custom?.baseUrl).toBe("https://config.example/v1");
|
||||
});
|
||||
});
|
||||
|
||||
it("replaces stale merged apiKey when provider is SecretRef-managed in current config", async () => {
|
||||
await withTempHome(async () => {
|
||||
await writeAgentModelsJson({
|
||||
|
||||
@@ -19,10 +19,14 @@ import {
|
||||
} from "./models-config.providers.js";
|
||||
|
||||
type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
|
||||
type ExistingProviderConfig = NonNullable<ModelsConfig["providers"]>[string] & {
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
api?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_MODE: NonNullable<ModelsConfig["mode"]> = "merge";
|
||||
const MODELS_JSON_WRITE_LOCKS = new Map<string, Promise<void>>();
|
||||
const AUTHORITATIVE_IMPLICIT_BASEURL_PROVIDERS = new Set(["openai-codex"]);
|
||||
|
||||
function isPositiveFiniteTokenLimit(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
||||
@@ -142,18 +146,10 @@ async function readJson(pathname: string): Promise<unknown> {
|
||||
async function resolveProvidersForModelsJson(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentDir: string;
|
||||
}): Promise<{
|
||||
providers: Record<string, ProviderConfig>;
|
||||
authoritativeImplicitBaseUrlProviders: ReadonlySet<string>;
|
||||
}> {
|
||||
}): Promise<Record<string, ProviderConfig>> {
|
||||
const { cfg, agentDir } = params;
|
||||
const explicitProviders = cfg.models?.providers ?? {};
|
||||
const implicitProviders = await resolveImplicitProviders({ agentDir, explicitProviders });
|
||||
const authoritativeImplicitBaseUrlProviders = new Set<string>(
|
||||
[...AUTHORITATIVE_IMPLICIT_BASEURL_PROVIDERS].filter((key) =>
|
||||
Boolean(implicitProviders?.[key]),
|
||||
),
|
||||
);
|
||||
const providers: Record<string, ProviderConfig> = mergeProviders({
|
||||
implicit: implicitProviders,
|
||||
explicit: explicitProviders,
|
||||
@@ -171,52 +167,80 @@ async function resolveProvidersForModelsJson(params: {
|
||||
if (implicitCopilot && !providers["github-copilot"]) {
|
||||
providers["github-copilot"] = implicitCopilot;
|
||||
}
|
||||
return { providers, authoritativeImplicitBaseUrlProviders };
|
||||
return providers;
|
||||
}
|
||||
|
||||
function resolveProviderApi(entry: { api?: unknown } | undefined): string | undefined {
|
||||
if (typeof entry?.api !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const api = entry.api.trim();
|
||||
return api || undefined;
|
||||
}
|
||||
|
||||
function shouldPreserveExistingApiKey(params: {
|
||||
providerKey: string;
|
||||
existing: ExistingProviderConfig;
|
||||
secretRefManagedProviders: ReadonlySet<string>;
|
||||
}): boolean {
|
||||
const { providerKey, existing, secretRefManagedProviders } = params;
|
||||
return (
|
||||
!secretRefManagedProviders.has(providerKey) &&
|
||||
typeof existing.apiKey === "string" &&
|
||||
existing.apiKey.length > 0 &&
|
||||
!isNonSecretApiKeyMarker(existing.apiKey, { includeEnvVarName: false })
|
||||
);
|
||||
}
|
||||
|
||||
function shouldPreserveExistingBaseUrl(params: {
|
||||
providerKey: string;
|
||||
existing: ExistingProviderConfig;
|
||||
nextEntry: ProviderConfig;
|
||||
explicitBaseUrlProviders: ReadonlySet<string>;
|
||||
}): boolean {
|
||||
const { providerKey, existing, nextEntry, explicitBaseUrlProviders } = params;
|
||||
if (
|
||||
explicitBaseUrlProviders.has(providerKey) ||
|
||||
typeof existing.baseUrl !== "string" ||
|
||||
existing.baseUrl.length === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existingApi = resolveProviderApi(existing);
|
||||
const nextApi = resolveProviderApi(nextEntry);
|
||||
return !existingApi || !nextApi || existingApi === nextApi;
|
||||
}
|
||||
|
||||
function mergeWithExistingProviderSecrets(params: {
|
||||
nextProviders: Record<string, ProviderConfig>;
|
||||
existingProviders: Record<string, NonNullable<ModelsConfig["providers"]>[string]>;
|
||||
existingProviders: Record<string, ExistingProviderConfig>;
|
||||
secretRefManagedProviders: ReadonlySet<string>;
|
||||
explicitBaseUrlProviders: ReadonlySet<string>;
|
||||
authoritativeImplicitBaseUrlProviders: ReadonlySet<string>;
|
||||
}): Record<string, ProviderConfig> {
|
||||
const {
|
||||
nextProviders,
|
||||
existingProviders,
|
||||
secretRefManagedProviders,
|
||||
explicitBaseUrlProviders,
|
||||
authoritativeImplicitBaseUrlProviders,
|
||||
} = params;
|
||||
const { nextProviders, existingProviders, secretRefManagedProviders, explicitBaseUrlProviders } =
|
||||
params;
|
||||
const mergedProviders: Record<string, ProviderConfig> = {};
|
||||
for (const [key, entry] of Object.entries(existingProviders)) {
|
||||
mergedProviders[key] = entry;
|
||||
}
|
||||
for (const [key, newEntry] of Object.entries(nextProviders)) {
|
||||
const existing = existingProviders[key] as
|
||||
| (NonNullable<ModelsConfig["providers"]>[string] & {
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
})
|
||||
| undefined;
|
||||
const existing = existingProviders[key];
|
||||
if (!existing) {
|
||||
mergedProviders[key] = newEntry;
|
||||
continue;
|
||||
}
|
||||
const preserved: Record<string, unknown> = {};
|
||||
if (
|
||||
!secretRefManagedProviders.has(key) &&
|
||||
typeof existing.apiKey === "string" &&
|
||||
existing.apiKey &&
|
||||
!isNonSecretApiKeyMarker(existing.apiKey, { includeEnvVarName: false })
|
||||
) {
|
||||
if (shouldPreserveExistingApiKey({ providerKey: key, existing, secretRefManagedProviders })) {
|
||||
preserved.apiKey = existing.apiKey;
|
||||
}
|
||||
if (
|
||||
!authoritativeImplicitBaseUrlProviders.has(key) &&
|
||||
!explicitBaseUrlProviders.has(key) &&
|
||||
typeof existing.baseUrl === "string" &&
|
||||
existing.baseUrl
|
||||
shouldPreserveExistingBaseUrl({
|
||||
providerKey: key,
|
||||
existing,
|
||||
nextEntry: newEntry,
|
||||
explicitBaseUrlProviders,
|
||||
})
|
||||
) {
|
||||
preserved.baseUrl = existing.baseUrl;
|
||||
}
|
||||
@@ -231,7 +255,6 @@ async function resolveProvidersForMode(params: {
|
||||
providers: Record<string, ProviderConfig>;
|
||||
secretRefManagedProviders: ReadonlySet<string>;
|
||||
explicitBaseUrlProviders: ReadonlySet<string>;
|
||||
authoritativeImplicitBaseUrlProviders: ReadonlySet<string>;
|
||||
}): Promise<Record<string, ProviderConfig>> {
|
||||
if (params.mode !== "merge") {
|
||||
return params.providers;
|
||||
@@ -246,10 +269,9 @@ async function resolveProvidersForMode(params: {
|
||||
>;
|
||||
return mergeWithExistingProviderSecrets({
|
||||
nextProviders: params.providers,
|
||||
existingProviders,
|
||||
existingProviders: existingProviders as Record<string, ExistingProviderConfig>,
|
||||
secretRefManagedProviders: params.secretRefManagedProviders,
|
||||
explicitBaseUrlProviders: params.explicitBaseUrlProviders,
|
||||
authoritativeImplicitBaseUrlProviders: params.authoritativeImplicitBaseUrlProviders,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -316,8 +338,7 @@ export async function ensureOpenClawModelsJson(
|
||||
// through the full loadConfig() pipeline which applies these.
|
||||
applyConfigEnvVars(cfg);
|
||||
|
||||
const { providers, authoritativeImplicitBaseUrlProviders } =
|
||||
await resolveProvidersForModelsJson({ cfg, agentDir });
|
||||
const providers = await resolveProvidersForModelsJson({ cfg, agentDir });
|
||||
|
||||
if (Object.keys(providers).length === 0) {
|
||||
return { agentDir, wrote: false };
|
||||
@@ -348,7 +369,6 @@ export async function ensureOpenClawModelsJson(
|
||||
providers: normalizedProviders,
|
||||
secretRefManagedProviders,
|
||||
explicitBaseUrlProviders,
|
||||
authoritativeImplicitBaseUrlProviders,
|
||||
});
|
||||
const next = `${JSON.stringify({ providers: mergedProviders }, null, 2)}\n`;
|
||||
const existingRaw = await readRawFile(targetPath);
|
||||
|
||||
@@ -803,7 +803,11 @@ describe("applyExtraParamsToAgent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes anthropic tool_choice modes for kimi-coding endpoints", () => {
|
||||
it.each([
|
||||
{ input: { type: "auto" }, expected: "auto" },
|
||||
{ input: { type: "none" }, expected: "none" },
|
||||
{ input: { type: "required" }, expected: "required" },
|
||||
])("normalizes anthropic tool_choice %j for kimi-coding endpoints", ({ input, expected }) => {
|
||||
const payloads: Record<string, unknown>[] = [];
|
||||
const baseStreamFn: StreamFn = (_model, _context, options) => {
|
||||
const payload: Record<string, unknown> = {
|
||||
@@ -814,7 +818,7 @@ describe("applyExtraParamsToAgent", () => {
|
||||
input_schema: { type: "object", properties: {} },
|
||||
},
|
||||
],
|
||||
tool_choice: { type: "auto" },
|
||||
tool_choice: input,
|
||||
};
|
||||
options?.onPayload?.(payload);
|
||||
payloads.push(payload);
|
||||
@@ -834,7 +838,7 @@ describe("applyExtraParamsToAgent", () => {
|
||||
void agent.streamFn?.(model, context, {});
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.tool_choice).toBe("auto");
|
||||
expect(payloads[0]?.tool_choice).toBe(expected);
|
||||
});
|
||||
|
||||
it("does not rewrite anthropic tool schema for non-kimi endpoints", () => {
|
||||
|
||||
@@ -3,6 +3,10 @@ import type { SimpleStreamOptions } from "@mariozechner/pi-ai";
|
||||
import { streamSimple } from "@mariozechner/pi-ai";
|
||||
import type { ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
usesOpenAiFunctionAnthropicToolSchema,
|
||||
usesOpenAiStringModeAnthropicToolChoice,
|
||||
} from "../provider-capabilities.js";
|
||||
import { log } from "./logger.js";
|
||||
|
||||
const OPENROUTER_APP_HEADERS: Record<string, string> = {
|
||||
@@ -786,7 +790,7 @@ function createMoonshotThinkingWrapper(
|
||||
};
|
||||
}
|
||||
|
||||
function isKimiCodingAnthropicEndpoint(model: {
|
||||
function requiresAnthropicToolPayloadCompatibility(model: {
|
||||
api?: unknown;
|
||||
provider?: unknown;
|
||||
baseUrl?: unknown;
|
||||
@@ -795,7 +799,7 @@ function isKimiCodingAnthropicEndpoint(model: {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof model.provider === "string" && model.provider.trim().toLowerCase() === "kimi-coding") {
|
||||
if (typeof model.provider === "string" && usesOpenAiFunctionAnthropicToolSchema(model.provider)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -814,7 +818,9 @@ function isKimiCodingAnthropicEndpoint(model: {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeKimiCodingToolDefinition(tool: unknown): Record<string, unknown> | undefined {
|
||||
function normalizeOpenAiFunctionAnthropicToolDefinition(
|
||||
tool: unknown,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!tool || typeof tool !== "object" || Array.isArray(tool)) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -852,7 +858,7 @@ function normalizeKimiCodingToolDefinition(tool: unknown): Record<string, unknow
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeKimiCodingToolChoice(toolChoice: unknown): unknown {
|
||||
function normalizeOpenAiStringModeAnthropicToolChoice(toolChoice: unknown): unknown {
|
||||
if (!toolChoice || typeof toolChoice !== "object" || Array.isArray(toolChoice)) {
|
||||
return toolChoice;
|
||||
}
|
||||
@@ -881,24 +887,43 @@ function normalizeKimiCodingToolChoice(toolChoice: unknown): unknown {
|
||||
}
|
||||
|
||||
/**
|
||||
* Kimi Coding's anthropic-messages endpoint expects OpenAI-style tool payloads
|
||||
* (`tools[].function`) even when messages use Anthropic request framing.
|
||||
* Some anthropic-messages providers accept Anthropic framing but still expect
|
||||
* OpenAI-style tool payloads (`tools[].function`, string tool_choice modes).
|
||||
*/
|
||||
function createKimiCodingAnthropicToolSchemaWrapper(baseStreamFn: StreamFn | undefined): StreamFn {
|
||||
function createAnthropicToolPayloadCompatibilityWrapper(
|
||||
baseStreamFn: StreamFn | undefined,
|
||||
): StreamFn {
|
||||
const underlying = baseStreamFn ?? streamSimple;
|
||||
return (model, context, options) => {
|
||||
const originalOnPayload = options?.onPayload;
|
||||
return underlying(model, context, {
|
||||
...options,
|
||||
onPayload: (payload) => {
|
||||
if (payload && typeof payload === "object" && isKimiCodingAnthropicEndpoint(model)) {
|
||||
if (
|
||||
payload &&
|
||||
typeof payload === "object" &&
|
||||
requiresAnthropicToolPayloadCompatibility(model)
|
||||
) {
|
||||
const payloadObj = payload as Record<string, unknown>;
|
||||
if (Array.isArray(payloadObj.tools)) {
|
||||
if (
|
||||
Array.isArray(payloadObj.tools) &&
|
||||
usesOpenAiFunctionAnthropicToolSchema(
|
||||
typeof model.provider === "string" ? model.provider : undefined,
|
||||
)
|
||||
) {
|
||||
payloadObj.tools = payloadObj.tools
|
||||
.map((tool) => normalizeKimiCodingToolDefinition(tool))
|
||||
.map((tool) => normalizeOpenAiFunctionAnthropicToolDefinition(tool))
|
||||
.filter((tool): tool is Record<string, unknown> => !!tool);
|
||||
}
|
||||
payloadObj.tool_choice = normalizeKimiCodingToolChoice(payloadObj.tool_choice);
|
||||
if (
|
||||
usesOpenAiStringModeAnthropicToolChoice(
|
||||
typeof model.provider === "string" ? model.provider : undefined,
|
||||
)
|
||||
) {
|
||||
payloadObj.tool_choice = normalizeOpenAiStringModeAnthropicToolChoice(
|
||||
payloadObj.tool_choice,
|
||||
);
|
||||
}
|
||||
}
|
||||
originalOnPayload?.(payload);
|
||||
},
|
||||
@@ -1245,7 +1270,7 @@ export function applyExtraParamsToAgent(
|
||||
agent.streamFn = createMoonshotThinkingWrapper(agent.streamFn, moonshotThinkingType);
|
||||
}
|
||||
|
||||
agent.streamFn = createKimiCodingAnthropicToolSchemaWrapper(agent.streamFn);
|
||||
agent.streamFn = createAnthropicToolPayloadCompatibilityWrapper(agent.streamFn);
|
||||
|
||||
if (provider === "openrouter") {
|
||||
log.debug(`applying OpenRouter app attribution headers for ${provider}/${modelId}`);
|
||||
|
||||
23
src/agents/provider-capabilities.test.ts
Normal file
23
src/agents/provider-capabilities.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveProviderCapabilities } from "./provider-capabilities.js";
|
||||
|
||||
describe("resolveProviderCapabilities", () => {
|
||||
it("returns native anthropic defaults for ordinary providers", () => {
|
||||
expect(resolveProviderCapabilities("anthropic")).toEqual({
|
||||
anthropicToolSchemaMode: "native",
|
||||
anthropicToolChoiceMode: "native",
|
||||
preserveAnthropicThinkingSignatures: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes kimi aliases to the same capability set", () => {
|
||||
expect(resolveProviderCapabilities("kimi-coding")).toEqual(
|
||||
resolveProviderCapabilities("kimi-code"),
|
||||
);
|
||||
expect(resolveProviderCapabilities("kimi-code")).toEqual({
|
||||
anthropicToolSchemaMode: "openai-functions",
|
||||
anthropicToolChoiceMode: "openai-string-modes",
|
||||
preserveAnthropicThinkingSignatures: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
41
src/agents/provider-capabilities.ts
Normal file
41
src/agents/provider-capabilities.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
|
||||
export type ProviderCapabilities = {
|
||||
anthropicToolSchemaMode: "native" | "openai-functions";
|
||||
anthropicToolChoiceMode: "native" | "openai-string-modes";
|
||||
preserveAnthropicThinkingSignatures: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_PROVIDER_CAPABILITIES: ProviderCapabilities = {
|
||||
anthropicToolSchemaMode: "native",
|
||||
anthropicToolChoiceMode: "native",
|
||||
preserveAnthropicThinkingSignatures: true,
|
||||
};
|
||||
|
||||
const PROVIDER_CAPABILITIES: Record<string, Partial<ProviderCapabilities>> = {
|
||||
"kimi-coding": {
|
||||
anthropicToolSchemaMode: "openai-functions",
|
||||
anthropicToolChoiceMode: "openai-string-modes",
|
||||
preserveAnthropicThinkingSignatures: false,
|
||||
},
|
||||
};
|
||||
|
||||
export function resolveProviderCapabilities(provider?: string | null): ProviderCapabilities {
|
||||
const normalized = normalizeProviderId(provider ?? "");
|
||||
return {
|
||||
...DEFAULT_PROVIDER_CAPABILITIES,
|
||||
...PROVIDER_CAPABILITIES[normalized],
|
||||
};
|
||||
}
|
||||
|
||||
export function preservesAnthropicThinkingSignatures(provider?: string | null): boolean {
|
||||
return resolveProviderCapabilities(provider).preserveAnthropicThinkingSignatures;
|
||||
}
|
||||
|
||||
export function usesOpenAiFunctionAnthropicToolSchema(provider?: string | null): boolean {
|
||||
return resolveProviderCapabilities(provider).anthropicToolSchemaMode === "openai-functions";
|
||||
}
|
||||
|
||||
export function usesOpenAiStringModeAnthropicToolChoice(provider?: string | null): boolean {
|
||||
return resolveProviderCapabilities(provider).anthropicToolChoiceMode === "openai-string-modes";
|
||||
}
|
||||
@@ -78,57 +78,58 @@ describe("resolveTranscriptPolicy", () => {
|
||||
expect(policy.sanitizeMode).toBe("full");
|
||||
});
|
||||
|
||||
it("preserves thinking signatures for Anthropic provider (#32526)", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
it.each([
|
||||
{
|
||||
title: "Anthropic provider",
|
||||
provider: "anthropic",
|
||||
modelId: "claude-opus-4-5",
|
||||
modelApi: "anthropic-messages",
|
||||
});
|
||||
expect(policy.preserveSignatures).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves thinking signatures for Bedrock Anthropic (#32526)", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
modelApi: "anthropic-messages" as const,
|
||||
preserveSignatures: true,
|
||||
},
|
||||
{
|
||||
title: "Bedrock Anthropic",
|
||||
provider: "amazon-bedrock",
|
||||
modelId: "us.anthropic.claude-opus-4-6-v1",
|
||||
modelApi: "bedrock-converse-stream",
|
||||
});
|
||||
expect(policy.preserveSignatures).toBe(true);
|
||||
});
|
||||
|
||||
it("does not preserve signatures for Google provider (#32526)", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
modelApi: "bedrock-converse-stream" as const,
|
||||
preserveSignatures: true,
|
||||
},
|
||||
{
|
||||
title: "Google provider",
|
||||
provider: "google",
|
||||
modelId: "gemini-2.0-flash",
|
||||
modelApi: "google-generative-ai",
|
||||
});
|
||||
expect(policy.preserveSignatures).toBe(false);
|
||||
});
|
||||
|
||||
it("does not preserve signatures for OpenAI provider (#32526)", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
modelApi: "google-generative-ai" as const,
|
||||
preserveSignatures: false,
|
||||
},
|
||||
{
|
||||
title: "OpenAI provider",
|
||||
provider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
modelApi: "openai",
|
||||
});
|
||||
expect(policy.preserveSignatures).toBe(false);
|
||||
});
|
||||
|
||||
it("does not preserve signatures for Mistral provider (#32526)", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
modelApi: "openai" as const,
|
||||
preserveSignatures: false,
|
||||
},
|
||||
{
|
||||
title: "Mistral provider",
|
||||
provider: "mistral",
|
||||
modelId: "mistral-large-latest",
|
||||
});
|
||||
expect(policy.preserveSignatures).toBe(false);
|
||||
});
|
||||
|
||||
it("does not preserve signatures for kimi-coding provider (#39798)", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
preserveSignatures: false,
|
||||
},
|
||||
{
|
||||
title: "kimi-coding provider",
|
||||
provider: "kimi-coding",
|
||||
modelId: "k2p5",
|
||||
modelApi: "anthropic-messages",
|
||||
});
|
||||
expect(policy.preserveSignatures).toBe(false);
|
||||
modelApi: "anthropic-messages" as const,
|
||||
preserveSignatures: false,
|
||||
},
|
||||
{
|
||||
title: "kimi-code alias",
|
||||
provider: "kimi-code",
|
||||
modelId: "k2p5",
|
||||
modelApi: "anthropic-messages" as const,
|
||||
preserveSignatures: false,
|
||||
},
|
||||
])("sets preserveSignatures for $title (#32526, #39798)", ({ preserveSignatures, ...input }) => {
|
||||
const policy = resolveTranscriptPolicy(input);
|
||||
expect(policy.preserveSignatures).toBe(preserveSignatures);
|
||||
});
|
||||
|
||||
it("enables turn-ordering and assistant-merge for strict OpenAI-compatible providers (#38962)", () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
import { isGoogleModelApi } from "./pi-embedded-helpers/google.js";
|
||||
import { preservesAnthropicThinkingSignatures } from "./provider-capabilities.js";
|
||||
import type { ToolCallIdMode } from "./tool-call-id.js";
|
||||
|
||||
export type TranscriptSanitizeMode = "full" | "images-only";
|
||||
@@ -39,8 +40,6 @@ const OPENAI_MODEL_APIS = new Set([
|
||||
]);
|
||||
const OPENAI_PROVIDERS = new Set(["openai", "openai-codex"]);
|
||||
const OPENAI_COMPAT_TURN_MERGE_EXCLUDED_PROVIDERS = new Set(["openrouter", "opencode"]);
|
||||
// Providers that use anthropic-messages API but cannot handle re-sent thinkingSignature blobs (#39798)
|
||||
const ANTHROPIC_API_SIGNATURE_EXCLUDED_PROVIDERS = new Set(["kimi-coding"]);
|
||||
|
||||
function isOpenAiApi(modelApi?: string | null): boolean {
|
||||
if (!modelApi) {
|
||||
@@ -125,7 +124,7 @@ export function resolveTranscriptPolicy(params: {
|
||||
(!isOpenAi && sanitizeToolCallIds) || requiresOpenAiCompatibleToolIdSanitization,
|
||||
toolCallIdMode,
|
||||
repairToolUseResultPairing,
|
||||
preserveSignatures: isAnthropic && !ANTHROPIC_API_SIGNATURE_EXCLUDED_PROVIDERS.has(provider),
|
||||
preserveSignatures: isAnthropic && preservesAnthropicThinkingSignatures(provider),
|
||||
sanitizeThoughtSignatures: isOpenAi ? undefined : sanitizeThoughtSignatures,
|
||||
sanitizeThinkingSignatures: false,
|
||||
dropThinkingBlocks,
|
||||
|
||||
Reference in New Issue
Block a user