fix(fireworks): route Fireworks models through resolveModel everywhere

pi-ai's getModel/getModels does not know about the Fireworks providers
(fireworks, fireworks-firepass); both live under our local model-resolver
because Fireworks speaks the Anthropic-compat endpoint with Bearer auth
at https://api.fireworks.ai/inference, not pi-ai's built-in catalog.

Three call sites were still hitting pi-ai directly and failing silently
for Fireworks:

1. connector-model-policy.ts — listProviderModels() caught the pi-ai
   throw and returned []. The "Fire Pass (CEO runtime)" card in the
   Team Models onboarding step therefore showed "暂无模型 / 已启用 0/0",
   blocking users on Fire Pass from continuing onboarding.

2. worker/worker-runner.ts — getModel() threw mid-run when the selected
   connector was Fireworks, so the worker could never actually execute
   an agent turn against Fire Pass or the curated Fireworks model set.
   Now uses resolveModel() + the Fireworks-specific Bearer header from
   resolveExtraStreamHeaders() so streamSimple hits the correct endpoint
   with the correct auth.

3. agent-runtime/cost-writer.ts — cost observation silently wrote
   amount_usd=null for Fireworks runs because getModel() threw inside
   the try/catch. Now resolves via resolveModel() so the Fireworks
   curated contextWindow/maxTokens are visible to calculateCost().

Verification: pnpm typecheck 0 / pnpm lint 0-0 / pnpm build:verify 0-0
/ pnpm test:unit 508/508.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-17 09:51:38 -04:00
1 parent 84a3520ea8
commit e7e43bd0a6
3 files changed
+24 -12

No files matched your search

+3 -2
View File
@@ -1,6 +1,7 @@
import { calculateCost, getModel, type Usage } from "@mariozechner/pi-ai";
import { calculateCost, type Usage } from "@mariozechner/pi-ai";
import type { AppDatabase } from "@main/database";
import { getConnectorDefinition } from "@main/connectors";
import { resolveModel } from "@main/model-resolver";
export interface CostWriterDeps {
db: AppDatabase;
@@ -29,7 +30,7 @@ export class CostWriter {
try {
const definition = getConnectorDefinition(input.connectorId);
if (definition.provider) {
const model = getModel(definition.provider as never, input.modelId as never);
const model = resolveModel(definition.provider, input.modelId);
if (model) {
const cost = calculateCost(model, input.usage);
amountUsd = Number.isFinite(cost.total) ? cost.total : null;
+9 -7
View File
@@ -1,6 +1,6 @@
import { getModels } from "@mariozechner/pi-ai";
import type { ConnectorId } from "@shared/types";
import { getConnectorDefinition } from "./connectors";
import { resolveModels } from "./model-resolver";
interface ConnectorModelPolicyDeps {
getConnector: (id: ConnectorId) => { id: ConnectorId; label?: string | null; model: string | null };
@@ -26,13 +26,15 @@ function uniqueModels(models: Array<{ id: string; label: string }>) {
return Array.from(byId.values());
}
/**
* Returns the models available for a given pi-ai provider id, going through
* `resolveModels` so that providers not in pi-ai's built-in catalog (currently
* the two Fireworks connectors, which serve `accounts/fireworks/routers/...`
* and curated `accounts/fireworks/models/...` ids) are represented in the UI
* model picker instead of collapsing to an empty list.
*/
function listProviderModels(provider: string): Array<{ id: string; label: string }> {
try {
const models = getModels(provider as never);
return models.map((model) => ({ id: model.id, label: model.name ?? model.id }));
} catch {
return [];
}
return resolveModels(provider).map((model) => ({ id: model.id, label: model.name }));
}
export function getConnectorModelPolicy(
+12 -3
View File
@@ -1,7 +1,8 @@
import { Agent } from "@mariozechner/pi-agent-core";
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { getModel, streamSimple } from "@mariozechner/pi-ai";
import { streamSimple } from "@mariozechner/pi-ai";
import { createCodingTools } from "@mariozechner/pi-coding-agent";
import { resolveModel, resolveExtraStreamHeaders } from "../model-resolver";
import type { RunStatus } from "@shared/types";
import type { ChildMessage, RunJob } from "./worker-helpers";
import { applyEnvOverrides, buildRedactor } from "./worker-helpers";
@@ -73,8 +74,12 @@ export class WorkerRunner {
const restoreEnv = applyEnvOverrides(prepared.extraEnv);
try {
const model = getModel(prepared.provider as never, job.model as never);
const model = resolveModel(prepared.provider, job.model);
if (!model) {
throw new Error(`Unknown model ${job.model} for provider ${prepared.provider}.`);
}
resolvedModelId = model.id;
const extraHeaders = resolveExtraStreamHeaders(prepared.provider, prepared.apiKey);
const cwd = job.workspacePath ?? process.cwd();
const tools = createCodingTools(cwd) as never;
@@ -87,7 +92,11 @@ export class WorkerRunner {
messages: prepared.priorMessages,
},
streamFn: (m, messages, options) =>
streamSimple(m, messages, { ...options, apiKey: prepared.apiKey }),
streamSimple(m, messages, {
...options,
apiKey: prepared.apiKey,
headers: { ...(options?.headers ?? {}), ...(extraHeaders ?? {}) },
}),
beforeToolCall: async (context) => {
const verdict = await rpc.invoke(job.runId, "beforeToolCall", {
agentId: job.agentId,