openclaw/src/agents/pi-embedded-runner/model.ts
Alex Fallah 1980561bef fix: inherit api field from provider config for inline Bedrock models
When a model is defined inline in models.providers without an explicit api field,
the resolveModel function now correctly inherits the api from the provider config.
This fixes Bedrock model discovery which relies on api: bedrock-converse-stream.

- Look up provider config using inlineMatch.provider
- Merge api field with fallback chain: model.api → provider.api → openai-responses
- Add tests validating api inheritance for inline models

Fixes inline model resolution for providers like Amazon Bedrock that require
specific API implementations.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-25 22:36:35 -05:00

101 lines
3.8 KiB
TypeScript

import type { Api, Model } from "@mariozechner/pi-ai";
import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent";
import type { ClawdbotConfig } from "../../config/config.js";
import type { ModelDefinitionConfig } from "../../config/types.js";
import { resolveClawdbotAgentDir } from "../agent-paths.js";
import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
import { normalizeModelCompat } from "../model-compat.js";
import { normalizeProviderId } from "../model-selection.js";
type InlineModelEntry = ModelDefinitionConfig & { provider: string };
export function buildInlineProviderModels(
providers: Record<string, { models?: ModelDefinitionConfig[] }>,
): InlineModelEntry[] {
return Object.entries(providers).flatMap(([providerId, entry]) => {
const trimmed = providerId.trim();
if (!trimmed) return [];
return (entry?.models ?? []).map((model) => ({ ...model, provider: trimmed }));
});
}
export function buildModelAliasLines(cfg?: ClawdbotConfig) {
const models = cfg?.agents?.defaults?.models ?? {};
const entries: Array<{ alias: string; model: string }> = [];
for (const [keyRaw, entryRaw] of Object.entries(models)) {
const model = String(keyRaw ?? "").trim();
if (!model) continue;
const alias = String((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim();
if (!alias) continue;
entries.push({ alias, model });
}
return entries
.sort((a, b) => a.alias.localeCompare(b.alias))
.map((entry) => `- ${entry.alias}: ${entry.model}`);
}
export function resolveModel(
provider: string,
modelId: string,
agentDir?: string,
cfg?: ClawdbotConfig,
): {
model?: Model<Api>;
error?: string;
authStorage: ReturnType<typeof discoverAuthStorage>;
modelRegistry: ReturnType<typeof discoverModels>;
} {
const resolvedAgentDir = agentDir ?? resolveClawdbotAgentDir();
const authStorage = discoverAuthStorage(resolvedAgentDir);
const modelRegistry = discoverModels(authStorage, resolvedAgentDir);
const model = modelRegistry.find(provider, modelId) as Model<Api> | null;
if (!model) {
const providers = cfg?.models?.providers ?? {};
const inlineModels = buildInlineProviderModels(providers);
const normalizedProvider = normalizeProviderId(provider);
const inlineMatch = inlineModels.find(
(entry) => normalizeProviderId(entry.provider) === normalizedProvider && entry.id === modelId,
);
if (inlineMatch) {
// Use inlineMatch.provider to look up config (preserves original key casing)
const providerCfg = providers[inlineMatch.provider];
// Inherit api from provider config if not set on the model itself
const apiValue = (inlineMatch.api ??
(providerCfg?.api as Api | undefined) ??
"openai-responses") as Api;
const modelWithApi = {
...inlineMatch,
api: apiValue,
};
const normalized = normalizeModelCompat(modelWithApi as Model<Api>);
return {
model: normalized,
authStorage,
modelRegistry,
};
}
const providerCfg = providers[provider];
if (providerCfg || modelId.startsWith("mock-")) {
const fallbackModel: Model<Api> = normalizeModelCompat({
id: modelId,
name: modelId,
api: providerCfg?.api ?? "openai-responses",
provider,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: providerCfg?.models?.[0]?.contextWindow ?? DEFAULT_CONTEXT_TOKENS,
maxTokens: providerCfg?.models?.[0]?.maxTokens ?? DEFAULT_CONTEXT_TOKENS,
} as Model<Api>);
return { model: fallbackModel, authStorage, modelRegistry };
}
return {
error: `Unknown model: ${provider}/${modelId}`,
authStorage,
modelRegistry,
};
}
return { model: normalizeModelCompat(model), authStorage, modelRegistry };
}