mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 10:37:27 +00:00
fix(model): propagate custom provider headers to model objects (#27490)
Merged via squash.
Prepared head SHA: e4183b398f
Co-authored-by: Sid-Qin <201593046+Sid-Qin@users.noreply.github.com>
Co-authored-by: shakkernerd <165377636+shakkernerd@users.noreply.github.com>
Reviewed-by: @shakkernerd
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Models/custom provider headers: propagate `models.providers.<name>.headers` across inline, fallback, and registry-found model resolution so header-authenticated proxies consistently receive configured request headers. (#27490) thanks @Sid-Qin.
|
||||
- Slack/system-event session routing: resolve reaction/member/pin/interaction system-event session keys through channel/account bindings (with sender-aware DM routing) so inbound Slack events target the correct agent session in multi-account setups instead of defaulting to `agent:main`. (#34045) Thanks @paulomcg, @daht-mad and @vincentkoc.
|
||||
- Gateway/HTTP tools invoke media compatibility: preserve raw media payload access for direct `/tools/invoke` clients by allowing media `nodes` invoke commands only in HTTP tool context, while keeping agent-context media invoke blocking to prevent base64 prompt bloat. (#34365) Thanks @obviyus.
|
||||
- Agents/Nodes media outputs: add dedicated `photos_latest` action handling, block media-returning `nodes invoke` commands, keep metadata-only `camera.list` invoke allowed, and normalize empty `photos_latest` results to a consistent response shape to prevent base64 context bloat. (#34332) Thanks @obviyus.
|
||||
|
||||
@@ -149,6 +149,36 @@ describe("buildInlineProviderModels", () => {
|
||||
name: "claude-opus-4.5",
|
||||
});
|
||||
});
|
||||
|
||||
it("merges provider-level headers into inline models", () => {
|
||||
const providers: Parameters<typeof buildInlineProviderModels>[0] = {
|
||||
proxy: {
|
||||
baseUrl: "https://proxy.example.com",
|
||||
api: "anthropic-messages",
|
||||
headers: { "User-Agent": "custom-agent/1.0" },
|
||||
models: [makeModel("claude-sonnet-4-6")],
|
||||
},
|
||||
};
|
||||
|
||||
const result = buildInlineProviderModels(providers);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].headers).toEqual({ "User-Agent": "custom-agent/1.0" });
|
||||
});
|
||||
|
||||
it("omits headers when neither provider nor model specifies them", () => {
|
||||
const providers: Parameters<typeof buildInlineProviderModels>[0] = {
|
||||
plain: {
|
||||
baseUrl: "http://localhost:8000",
|
||||
models: [makeModel("some-model")],
|
||||
},
|
||||
};
|
||||
|
||||
const result = buildInlineProviderModels(providers);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].headers).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveModel", () => {
|
||||
@@ -171,6 +201,28 @@ describe("resolveModel", () => {
|
||||
expect(result.model?.id).toBe("missing-model");
|
||||
});
|
||||
|
||||
it("includes provider headers in provider fallback model", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
custom: {
|
||||
baseUrl: "http://localhost:9000",
|
||||
headers: { "X-Custom-Auth": "token-123" },
|
||||
models: [makeModel("listed-model")],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
// Requesting a non-listed model forces the providerCfg fallback branch.
|
||||
const result = resolveModel("custom", "missing-model", "/tmp/agent", cfg);
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect((result.model as unknown as { headers?: Record<string, string> }).headers).toEqual({
|
||||
"X-Custom-Auth": "token-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers matching configured model metadata for fallback token limits", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
@@ -379,4 +431,80 @@ describe("resolveModel", () => {
|
||||
expect(result.model).toBeUndefined();
|
||||
expect(result.error).toBe("Unknown model: google-antigravity/some-model");
|
||||
});
|
||||
|
||||
it("applies provider baseUrl override to registry-found models", () => {
|
||||
mockDiscoveredModel({
|
||||
provider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
templateModel: buildForwardCompatTemplate({
|
||||
id: "claude-sonnet-4-5",
|
||||
name: "Claude Sonnet 4.5",
|
||||
provider: "anthropic",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
}),
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
anthropic: {
|
||||
baseUrl: "https://my-proxy.example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const result = resolveModel("anthropic", "claude-sonnet-4-5", "/tmp/agent", cfg);
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.model?.baseUrl).toBe("https://my-proxy.example.com");
|
||||
});
|
||||
|
||||
it("applies provider headers override to registry-found models", () => {
|
||||
mockDiscoveredModel({
|
||||
provider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
templateModel: buildForwardCompatTemplate({
|
||||
id: "claude-sonnet-4-5",
|
||||
name: "Claude Sonnet 4.5",
|
||||
provider: "anthropic",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
}),
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
anthropic: {
|
||||
headers: { "X-Custom-Auth": "token-123" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const result = resolveModel("anthropic", "claude-sonnet-4-5", "/tmp/agent", cfg);
|
||||
expect(result.error).toBeUndefined();
|
||||
expect((result.model as unknown as { headers?: Record<string, string> }).headers).toEqual({
|
||||
"X-Custom-Auth": "token-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not override when no provider config exists", () => {
|
||||
mockDiscoveredModel({
|
||||
provider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
templateModel: buildForwardCompatTemplate({
|
||||
id: "claude-sonnet-4-5",
|
||||
name: "Claude Sonnet 4.5",
|
||||
provider: "anthropic",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
}),
|
||||
});
|
||||
|
||||
const result = resolveModel("anthropic", "claude-sonnet-4-5", "/tmp/agent");
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.model?.baseUrl).toBe("https://api.anthropic.com");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,11 +13,13 @@ import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
|
||||
type InlineModelEntry = ModelDefinitionConfig & {
|
||||
provider: string;
|
||||
baseUrl?: string;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
type InlineProviderConfig = {
|
||||
baseUrl?: string;
|
||||
api?: ModelDefinitionConfig["api"];
|
||||
models?: ModelDefinitionConfig[];
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
export { buildModelAliasLines };
|
||||
@@ -35,6 +37,10 @@ export function buildInlineProviderModels(
|
||||
provider: trimmed,
|
||||
baseUrl: entry?.baseUrl,
|
||||
api: model.api ?? entry?.api,
|
||||
headers:
|
||||
entry?.headers || (model as InlineModelEntry).headers
|
||||
? { ...entry?.headers, ...(model as InlineModelEntry).headers }
|
||||
: undefined,
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -114,6 +120,10 @@ export function resolveModel(
|
||||
configuredModel?.maxTokens ??
|
||||
providerCfg?.models?.[0]?.maxTokens ??
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
headers:
|
||||
providerCfg?.headers || configuredModel?.headers
|
||||
? { ...providerCfg?.headers, ...configuredModel?.headers }
|
||||
: undefined,
|
||||
} as Model<Api>);
|
||||
return { model: fallbackModel, authStorage, modelRegistry };
|
||||
}
|
||||
@@ -123,6 +133,20 @@ export function resolveModel(
|
||||
modelRegistry,
|
||||
};
|
||||
}
|
||||
const providerOverride = cfg?.models?.providers?.[provider] as InlineProviderConfig | undefined;
|
||||
if (providerOverride?.baseUrl || providerOverride?.headers) {
|
||||
const overridden: Model<Api> & { headers?: Record<string, string> } = { ...model };
|
||||
if (providerOverride.baseUrl) {
|
||||
overridden.baseUrl = providerOverride.baseUrl;
|
||||
}
|
||||
if (providerOverride.headers) {
|
||||
overridden.headers = {
|
||||
...(model as Model<Api> & { headers?: Record<string, string> }).headers,
|
||||
...providerOverride.headers,
|
||||
};
|
||||
}
|
||||
return { model: normalizeModelCompat(overridden), authStorage, modelRegistry };
|
||||
}
|
||||
return { model: normalizeModelCompat(model), authStorage, modelRegistry };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user