mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 08:31:41 +00:00
fix: default custom provider model fields
This commit is contained in:
@@ -23,6 +23,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco.
|
- Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco.
|
||||||
- Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
|
- Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
|
||||||
- Agents: use the active auth profile for auto-compaction recovery.
|
- Agents: use the active auth profile for auto-compaction recovery.
|
||||||
|
- Models: default missing custom provider fields so minimal configs are accepted.
|
||||||
- Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b.
|
- Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b.
|
||||||
- macOS: default direct-transport `ws://` URLs to port 18789; document `gateway.remote.transport`. (#1603) Thanks @ngutman.
|
- macOS: default direct-transport `ws://` URLs to port 18789; document `gateway.remote.transport`. (#1603) Thanks @ngutman.
|
||||||
- Voice Call: return stream TwiML for outbound conversation calls on initial Twilio webhook. (#1634)
|
- Voice Call: return stream TwiML for outbound conversation calls on initial Twilio webhook. (#1634)
|
||||||
|
|||||||
@@ -295,6 +295,16 @@ Example (OpenAI‑compatible):
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- For custom providers, `reasoning`, `input`, `cost`, `contextWindow`, and `maxTokens` are optional.
|
||||||
|
When omitted, Clawdbot defaults to:
|
||||||
|
- `reasoning: false`
|
||||||
|
- `input: ["text"]`
|
||||||
|
- `cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }`
|
||||||
|
- `contextWindow: 200000`
|
||||||
|
- `maxTokens: 8192`
|
||||||
|
- Recommended: set explicit values that match your proxy/model limits.
|
||||||
|
|
||||||
## CLI examples
|
## CLI examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
|
||||||
import { parseModelRef } from "../agents/model-selection.js";
|
import { parseModelRef } from "../agents/model-selection.js";
|
||||||
import { resolveTalkApiKey } from "./talk.js";
|
import { resolveTalkApiKey } from "./talk.js";
|
||||||
import type { ClawdbotConfig } from "./types.js";
|
import type { ClawdbotConfig } from "./types.js";
|
||||||
import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js";
|
import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js";
|
||||||
|
import type { ModelDefinitionConfig } from "./types.models.js";
|
||||||
|
|
||||||
type WarnState = { warned: boolean };
|
type WarnState = { warned: boolean };
|
||||||
|
|
||||||
@@ -23,6 +25,34 @@ const DEFAULT_MODEL_ALIASES: Readonly<Record<string, string>> = {
|
|||||||
"gemini-flash": "google/gemini-3-flash-preview",
|
"gemini-flash": "google/gemini-3-flash-preview",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_MODEL_COST: ModelDefinitionConfig["cost"] = {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
};
|
||||||
|
const DEFAULT_MODEL_INPUT: ModelDefinitionConfig["input"] = ["text"];
|
||||||
|
const DEFAULT_MODEL_MAX_TOKENS = 8192;
|
||||||
|
|
||||||
|
type ModelDefinitionLike = Partial<ModelDefinitionConfig> &
|
||||||
|
Pick<ModelDefinitionConfig, "id" | "name">;
|
||||||
|
|
||||||
|
function isPositiveNumber(value: unknown): value is number {
|
||||||
|
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveModelCost(
|
||||||
|
raw?: Partial<ModelDefinitionConfig["cost"]>,
|
||||||
|
): ModelDefinitionConfig["cost"] {
|
||||||
|
return {
|
||||||
|
input: typeof raw?.input === "number" ? raw.input : DEFAULT_MODEL_COST.input,
|
||||||
|
output: typeof raw?.output === "number" ? raw.output : DEFAULT_MODEL_COST.output,
|
||||||
|
cacheRead: typeof raw?.cacheRead === "number" ? raw.cacheRead : DEFAULT_MODEL_COST.cacheRead,
|
||||||
|
cacheWrite:
|
||||||
|
typeof raw?.cacheWrite === "number" ? raw.cacheWrite : DEFAULT_MODEL_COST.cacheWrite,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function resolveAnthropicDefaultAuthMode(cfg: ClawdbotConfig): AnthropicAuthDefaultsMode | null {
|
function resolveAnthropicDefaultAuthMode(cfg: ClawdbotConfig): AnthropicAuthDefaultsMode | null {
|
||||||
const profiles = cfg.auth?.profiles ?? {};
|
const profiles = cfg.auth?.profiles ?? {};
|
||||||
const anthropicProfiles = Object.entries(profiles).filter(
|
const anthropicProfiles = Object.entries(profiles).filter(
|
||||||
@@ -114,12 +144,77 @@ export function applyTalkApiKey(config: ClawdbotConfig): ClawdbotConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function applyModelDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
export function applyModelDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||||
const existingAgent = cfg.agents?.defaults;
|
|
||||||
if (!existingAgent) return cfg;
|
|
||||||
const existingModels = existingAgent.models ?? {};
|
|
||||||
if (Object.keys(existingModels).length === 0) return cfg;
|
|
||||||
|
|
||||||
let mutated = false;
|
let mutated = false;
|
||||||
|
let nextCfg = cfg;
|
||||||
|
|
||||||
|
const providerConfig = nextCfg.models?.providers;
|
||||||
|
if (providerConfig) {
|
||||||
|
const nextProviders = { ...providerConfig };
|
||||||
|
for (const [providerId, provider] of Object.entries(providerConfig)) {
|
||||||
|
const models = provider.models;
|
||||||
|
if (!Array.isArray(models) || models.length === 0) continue;
|
||||||
|
let providerMutated = false;
|
||||||
|
const nextModels = models.map((model) => {
|
||||||
|
const raw = model as ModelDefinitionLike;
|
||||||
|
let modelMutated = false;
|
||||||
|
|
||||||
|
const reasoning = typeof raw.reasoning === "boolean" ? raw.reasoning : false;
|
||||||
|
if (raw.reasoning !== reasoning) modelMutated = true;
|
||||||
|
|
||||||
|
const input = raw.input ?? [...DEFAULT_MODEL_INPUT];
|
||||||
|
if (raw.input === undefined) modelMutated = true;
|
||||||
|
|
||||||
|
const cost = resolveModelCost(raw.cost);
|
||||||
|
const costMutated =
|
||||||
|
!raw.cost ||
|
||||||
|
raw.cost.input !== cost.input ||
|
||||||
|
raw.cost.output !== cost.output ||
|
||||||
|
raw.cost.cacheRead !== cost.cacheRead ||
|
||||||
|
raw.cost.cacheWrite !== cost.cacheWrite;
|
||||||
|
if (costMutated) modelMutated = true;
|
||||||
|
|
||||||
|
const contextWindow = isPositiveNumber(raw.contextWindow)
|
||||||
|
? raw.contextWindow
|
||||||
|
: DEFAULT_CONTEXT_TOKENS;
|
||||||
|
if (raw.contextWindow !== contextWindow) modelMutated = true;
|
||||||
|
|
||||||
|
const defaultMaxTokens = Math.min(DEFAULT_MODEL_MAX_TOKENS, contextWindow);
|
||||||
|
const maxTokens = isPositiveNumber(raw.maxTokens) ? raw.maxTokens : defaultMaxTokens;
|
||||||
|
if (raw.maxTokens !== maxTokens) modelMutated = true;
|
||||||
|
|
||||||
|
if (!modelMutated) return model;
|
||||||
|
providerMutated = true;
|
||||||
|
return {
|
||||||
|
...raw,
|
||||||
|
reasoning,
|
||||||
|
input,
|
||||||
|
cost,
|
||||||
|
contextWindow,
|
||||||
|
maxTokens,
|
||||||
|
} as ModelDefinitionConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!providerMutated) continue;
|
||||||
|
nextProviders[providerId] = { ...provider, models: nextModels };
|
||||||
|
mutated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mutated) {
|
||||||
|
nextCfg = {
|
||||||
|
...nextCfg,
|
||||||
|
models: {
|
||||||
|
...nextCfg.models,
|
||||||
|
providers: nextProviders,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingAgent = nextCfg.agents?.defaults;
|
||||||
|
if (!existingAgent) return mutated ? nextCfg : cfg;
|
||||||
|
const existingModels = existingAgent.models ?? {};
|
||||||
|
if (Object.keys(existingModels).length === 0) return mutated ? nextCfg : cfg;
|
||||||
|
|
||||||
const nextModels: Record<string, { alias?: string }> = {
|
const nextModels: Record<string, { alias?: string }> = {
|
||||||
...existingModels,
|
...existingModels,
|
||||||
};
|
};
|
||||||
@@ -135,9 +230,9 @@ export function applyModelDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
|||||||
if (!mutated) return cfg;
|
if (!mutated) return cfg;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...cfg,
|
...nextCfg,
|
||||||
agents: {
|
agents: {
|
||||||
...cfg.agents,
|
...nextCfg.agents,
|
||||||
defaults: { ...existingAgent, models: nextModels },
|
defaults: { ...existingAgent, models: nextModels },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
|
||||||
import { applyModelDefaults } from "./defaults.js";
|
import { applyModelDefaults } from "./defaults.js";
|
||||||
import type { ClawdbotConfig } from "./types.js";
|
import type { ClawdbotConfig } from "./types.js";
|
||||||
|
|
||||||
@@ -55,4 +56,28 @@ describe("applyModelDefaults", () => {
|
|||||||
"gemini-flash",
|
"gemini-flash",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("fills missing model provider defaults", () => {
|
||||||
|
const cfg = {
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
myproxy: {
|
||||||
|
baseUrl: "https://proxy.example/v1",
|
||||||
|
apiKey: "sk-test",
|
||||||
|
api: "openai-completions",
|
||||||
|
models: [{ id: "gpt-5.2", name: "GPT-5.2" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies ClawdbotConfig;
|
||||||
|
|
||||||
|
const next = applyModelDefaults(cfg);
|
||||||
|
const model = next.models?.providers?.myproxy?.models?.[0];
|
||||||
|
|
||||||
|
expect(model?.reasoning).toBe(false);
|
||||||
|
expect(model?.input).toEqual(["text"]);
|
||||||
|
expect(model?.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
||||||
|
expect(model?.contextWindow).toBe(DEFAULT_CONTEXT_TOKENS);
|
||||||
|
expect(model?.maxTokens).toBe(8192);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,18 +28,19 @@ export const ModelDefinitionSchema = z
|
|||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
api: ModelApiSchema.optional(),
|
api: ModelApiSchema.optional(),
|
||||||
reasoning: z.boolean(),
|
reasoning: z.boolean().optional(),
|
||||||
input: z.array(z.union([z.literal("text"), z.literal("image")])),
|
input: z.array(z.union([z.literal("text"), z.literal("image")])).optional(),
|
||||||
cost: z
|
cost: z
|
||||||
.object({
|
.object({
|
||||||
input: z.number(),
|
input: z.number().optional(),
|
||||||
output: z.number(),
|
output: z.number().optional(),
|
||||||
cacheRead: z.number(),
|
cacheRead: z.number().optional(),
|
||||||
cacheWrite: z.number(),
|
cacheWrite: z.number().optional(),
|
||||||
})
|
})
|
||||||
.strict(),
|
.strict()
|
||||||
contextWindow: z.number().positive(),
|
.optional(),
|
||||||
maxTokens: z.number().positive(),
|
contextWindow: z.number().positive().optional(),
|
||||||
|
maxTokens: z.number().positive().optional(),
|
||||||
headers: z.record(z.string(), z.string()).optional(),
|
headers: z.record(z.string(), z.string()).optional(),
|
||||||
compat: ModelCompatSchema,
|
compat: ModelCompatSchema,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user