Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/lib/actions/inference-set.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,58 @@ describe("patchHermesInferenceConfig", () => {
});
expect(config.terminal).toEqual({ backend: "local" });
});

it("propagates the anthropic wire mode for anthropic-prod routes", () => {
const config: ConfigObject = {
model: {
default: "nvidia-routed",
provider: "custom",
base_url: "https://inference.local/v1",
},
};

const result = patchHermesInferenceConfig(config, "anthropic-prod", "claude-sonnet-4-6");

expect(result.changed).toBe(true);
expect(config.model).toEqual({
default: "claude-sonnet-4-6",
provider: "custom",
base_url: "https://inference.local",
api_mode: "anthropic_messages",
});
});

it("propagates the anthropic wire mode for compatible-anthropic-endpoint routes", () => {
const config: ConfigObject = { model: {} };

patchHermesInferenceConfig(config, "compatible-anthropic-endpoint", "claude-sonnet-4-6");

expect(config.model).toEqual({
default: "claude-sonnet-4-6",
provider: "custom",
base_url: "https://inference.local",
api_mode: "anthropic_messages",
});
});

it("clears a stale anthropic wire mode when switching to an OpenAI-wire route", () => {
const config: ConfigObject = {
model: {
default: "claude-sonnet-4-6",
provider: "custom",
base_url: "https://inference.local",
api_mode: "anthropic_messages",
},
};

patchHermesInferenceConfig(config, "hermes-provider", "openai/gpt-5.4-mini");

expect(config.model).toEqual({
default: "openai/gpt-5.4-mini",
provider: "custom",
base_url: "https://inference.local/v1",
});
});
});

describe("runInferenceSet", () => {
Expand Down
13 changes: 13 additions & 0 deletions src/lib/actions/inference-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,19 @@ export function patchHermesInferenceConfig(
modelConfig.default = model;
modelConfig.base_url = route.inferenceBaseUrl;
modelConfig.provider = "custom";
// Hermes "custom" providers default to the OpenAI chat-completions wire and
// pick other transports only from explicit config or base-url heuristics —
// "https://inference.local/v1" matches none. Anthropic-wire routes
// (anthropic-prod, compatible-anthropic-endpoint) must therefore propagate
// the API mode, or the agent POSTs /chat/completions at an anthropic-type
// provider and the gateway L7 policy denies every inference call. Clear the
// key on non-anthropic routes so a stale wire mode never survives a provider
// switch (mirrors Hermes's own stale-api_mode cleanup on provider changes).
if (route.inferenceApi === "anthropic-messages") {
modelConfig.api_mode = "anthropic_messages";
} else {
delete modelConfig.api_mode;
}

return { changed: before !== JSON.stringify(config), route };
}
Expand Down
40 changes: 40 additions & 0 deletions src/lib/inference/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,46 @@ describe("getSandboxInferenceConfig", () => {
});
});

it("maps anthropic-prod to the anthropic wire behind inference.local", () => {
expect(getSandboxInferenceConfig("claude-sonnet-4-6", "anthropic-prod")).toEqual({
providerKey: "anthropic",
primaryModelRef: "anthropic/claude-sonnet-4-6",
inferenceBaseUrl: "https://inference.local",
inferenceApi: "anthropic-messages",
inferenceCompat: null,
});
});

it("defaults compatible-anthropic-endpoint to the anthropic wire when no API preference is baked", () => {
expect(getSandboxInferenceConfig("claude-sonnet-4-6", "compatible-anthropic-endpoint")).toEqual(
{
providerKey: "anthropic",
primaryModelRef: "anthropic/claude-sonnet-4-6",
inferenceBaseUrl: "https://inference.local",
inferenceApi: "anthropic-messages",
inferenceCompat: null,
},
);
});

it("keeps compatible-anthropic-endpoint on the OpenAI wire only for an explicit preference", () => {
expect(
getSandboxInferenceConfig(
"claude-sonnet-4-6",
"compatible-anthropic-endpoint",
"openai-completions",
),
).toEqual({
providerKey: MANAGED_PROVIDER_ID,
primaryModelRef: `${MANAGED_PROVIDER_ID}/claude-sonnet-4-6`,
inferenceBaseUrl: INFERENCE_ROUTE_URL,
inferenceApi: "openai-completions",
inferenceCompat: {
supportsStore: false,
},
});
});

it("maps OpenAI-compatible endpoints to the managed inference provider", () => {
expect(getSandboxInferenceConfig("deepseek-ai/DeepSeek-V4-Flash", "compatible-endpoint"))
.toEqual({
Expand Down
11 changes: 10 additions & 1 deletion src/lib/inference/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,16 @@ export function getSandboxInferenceConfig(
break;
case "anthropic-prod":
case "compatible-anthropic-endpoint":
if (provider === "compatible-anthropic-endpoint" && inferenceApi === "openai-completions") {
// Only an EXPLICIT openai-completions preference keeps an
// Anthropic-compatible endpoint on the OpenAI wire. The fallback default
// ("openai-completions" when preferredInferenceApi is null) must not:
// callers without a baked preference — notably the Hermes config sync,
// which never passes one — would otherwise speak the OpenAI protocol at
// an anthropic-type provider and be denied by the gateway L7 policy.
if (
provider === "compatible-anthropic-endpoint" &&
preferredInferenceApi === "openai-completions"
) {
providerKey = MANAGED_PROVIDER_ID;
primaryModelRef = `${MANAGED_PROVIDER_ID}/${model}`;
inferenceCompat = {
Expand Down
Loading