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
1 change: 1 addition & 0 deletions .github/workflows/sync-models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
DIGITALOCEAN_API_TOKEN: ${{ secrets.DIGITALOCEAN_API_TOKEN }}
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
HF_TOKEN: ${{ secrets.HF_TOKEN }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"vercel:generate": "bun ./packages/core/script/sync-models.ts vercel",
"wandb:generate": "bun ./packages/core/script/sync-models.ts wandb",
"digitalocean:sync": "bun ./packages/core/script/sync-models.ts digitalocean",
"fireworks:sync": "bun ./packages/core/script/sync-models.ts fireworks-ai",
"ambient:sync": "bun ./packages/core/script/sync-models.ts ambient",
"models:sync": "bun ./packages/core/script/sync-models.ts",
"sync:models": "bun ./packages/core/script/sync-models.ts",
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { deepinfra } from "./providers/deepinfra.js";
import { digitalocean } from "./providers/digitalocean.js";
import { edenai } from "./providers/edenai.js";
import { empiriolabs } from "./providers/empiriolabs.js";
import { fireworksAi } from "./providers/fireworks-ai.js";
import { githubCopilot } from "./providers/github-copilot.js";
import { google } from "./providers/google.js";
import { hyper } from "./providers/hyper.js";
Expand Down Expand Up @@ -142,6 +143,7 @@ export const providers: {
digitalocean: SyncProvider<any>;
edenai: SyncProvider<any>;
empiriolabs: SyncProvider<any>;
"fireworks-ai": SyncProvider<any>;
"github-copilot": SyncProvider<any>;
google: SyncProvider<any>;
hyper: SyncProvider<any>;
Expand Down Expand Up @@ -177,6 +179,7 @@ export const providers: {
digitalocean,
edenai,
empiriolabs,
"fireworks-ai": fireworksAi,
"github-copilot": githubCopilot,
google,
hyper,
Expand Down Expand Up @@ -219,7 +222,7 @@ export const groups = {
"vercel",
],
cloudflare: ["cloudflare-ai-gateway", "cloudflare-workers-ai"],
direct: ["ambient", "anthropic", "baseten", "chutes", "cortecs", "deepinfra", "digitalocean", "github-copilot", "google", "hyper", "meta", "openai", "ovhcloud", "pioneer", "tinfoil", "venice", "wandb", "xai"],
direct: ["ambient", "anthropic", "baseten", "chutes", "cortecs", "deepinfra", "digitalocean", "fireworks-ai", "github-copilot", "google", "hyper", "meta", "openai", "ovhcloud", "pioneer", "tinfoil", "venice", "wandb", "xai"],
} as const;

type ProviderID = keyof typeof providers;
Expand Down
168 changes: 168 additions & 0 deletions packages/core/src/sync/providers/fireworks-ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { z } from "zod";

import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
import { factorBaseModel } from "./openrouter.js";

const API_ENDPOINT = "https://api.fireworks.ai/inference/v1/models";

export const FireworksModel = z.object({
id: z.string().min(1),
object: z.literal("model"),
created: z.number().int().nonnegative(),
owned_by: z.string(),
context_length: z.number().int().positive().optional(),
kind: z.string(),
supports_chat: z.boolean(),
supports_image_input: z.boolean(),
supports_tools: z.boolean(),
}).passthrough();

export const FireworksResponse = z.object({
object: z.literal("list"),
data: z.array(FireworksModel),
}).passthrough();

export type FireworksModel = z.infer<typeof FireworksModel>;

export const fireworksAi = {
id: "fireworks-ai",
name: "Fireworks AI",
modelsDir: "providers/fireworks-ai/models",
skipCreates: true,
// The endpoint is filtered by account model-access policy. Never interpret
// an absent row as proof that Fireworks removed a public model.
deleteMissing: false,
sourceID(model) {
return supportsCatalogModel(model) ? model.id : undefined;
},
skippedNotice(ids) {
if (ids.length === 0) return [];
return [
`${ids.length} Fireworks text/vision models returned by the API were not created because the endpoint does not provide pricing, output limits, reasoning controls, or structured-output metadata. Existing models are still updated from API-authoritative fields.`,
`Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`,
];
},
missingNotice(paths) {
if (paths.length === 0) return [];
return [
`${paths.length} local Fireworks models were absent from this account's model list and were retained for manual lifecycle review.`,
`Retained local paths: ${paths.map((item) => `\`${item}\``).join(", ")}`,
];
},
async fetchModels() {
const key = process.env.FIREWORKS_API_KEY;
if (key === undefined) throw new Error("Fireworks AI sync requires FIREWORKS_API_KEY");
return fetchFireworksModels(key);
},
parseModels(raw) {
return FireworksResponse.parse(raw).data;
},
translateModel(model, context) {
if (!supportsCatalogModel(model)) return undefined;
const existing = context.existing(model.id);
if (existing === undefined) return undefined;
return {
id: model.id,
model: buildFireworksModel(model, existing),
};
},
} satisfies SyncProvider<FireworksModel>;

export async function fetchFireworksModels(
key: string,
fetcher: typeof fetch = fetch,
) {
const response = await fetcher(API_ENDPOINT, {
headers: { Authorization: `Bearer ${key}` },
});
if (!response.ok) {
throw new Error(`Fireworks AI models request failed: ${response.status} ${response.statusText}`);
}
return FireworksResponse.parse(await response.json());
}

function supportsCatalogModel(model: FireworksModel) {
return model.supports_chat && model.kind !== "EMBEDDING_MODEL";
}

type Modality = SyncedFullModel["modalities"]["input"][number];

function inputModalities(existing: Modality[], supportsImage: boolean): Modality[] {
if (!supportsImage || existing.includes("image")) return existing;
return [...existing, "image" as const];
}

export function buildFireworksModel(
model: FireworksModel,
existing: ExistingModel,
): SyncedModel {
const name = existing.name;
const description = existing.description;
const releaseDate = existing.release_date;
const lastUpdated = existing.last_updated;
const reasoning = existing.reasoning;
const toolCall = existing.tool_call;
const openWeights = existing.open_weights;
const limit = existing.limit;
const modalities = existing.modalities;
const cost = existing.cost;

if (
name === undefined
|| description === undefined
|| releaseDate === undefined
|| lastUpdated === undefined
|| reasoning === undefined
|| toolCall === undefined
|| openWeights === undefined
|| limit === undefined
|| limit.context === undefined
|| limit.output === undefined
|| modalities === undefined
|| cost === undefined
) {
throw new Error(`Fireworks AI model ${model.id} has incomplete local TOML metadata required for sync`);
}

const input = inputModalities(modalities.input, model.supports_image_input);
// Fireworks reports the advertised context window, while some deployments
// reserve a few prompt tokens. Preserve a smaller verified local cap, but
// immediately follow any lower ceiling reported by the API.
const context = model.context_length === undefined
? limit.context
: Math.min(limit.context, model.context_length);
const output = Math.min(limit.output, context);
const values = {
name,
description,
family: existing.family,
release_date: releaseDate,
last_updated: lastUpdated,
attachment: input.some((modality) => modality !== "text"),
reasoning,
reasoning_options: existing.reasoning_options,
temperature: existing.temperature,
tool_call: model.supports_tools || toolCall,
structured_output: existing.structured_output,
knowledge: existing.knowledge,
open_weights: openWeights,
status: existing.status,
interleaved: existing.interleaved,
cost,
limit: {
context,
input: limit.input,
output,
},
modalities: {
input,
output: modalities.output,
},
provider: existing.provider,
experimental: existing.experimental,
} satisfies SyncedFullModel;

return existing.base_model === undefined
? values
: factorBaseModel(existing.base_model, values, values.limit, existing.base_model_omit);
}
106 changes: 106 additions & 0 deletions packages/core/test/fireworks-ai-sync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { expect, test } from "bun:test";

import type { ExistingModel } from "../src/sync/index.js";
import {
buildFireworksModel,
FireworksResponse,
fireworksAi,
type FireworksModel,
} from "../src/sync/providers/fireworks-ai.js";

test("parses the Fireworks OpenAI-compatible model list", () => {
const parsed = FireworksResponse.parse({
object: "list",
data: [fireworksModel()],
});

expect(parsed.data[0]).toMatchObject({
id: "accounts/fireworks/models/example",
context_length: 1_048_576,
supports_tools: true,
});
});

test("adds positive Fireworks capabilities while preserving authored facts", () => {
const model = buildFireworksModel(
fireworksModel({ supports_image_input: true, supports_tools: false }),
existingModel(),
);

expect(model).toMatchObject({
attachment: true,
tool_call: true,
cost: { input: 1, output: 2 },
limit: { context: 1_048_573, output: 262_144 },
modalities: { input: ["text", "image"], output: ["text"] },
reasoning_options: [{ type: "effort", values: ["low", "high"] }],
});
});

test("does not remove image support from a false-negative Fireworks flag", () => {
const model = buildFireworksModel(
fireworksModel({ supports_image_input: false }),
{
...existingModel(),
attachment: true,
modalities: { input: ["text", "image", "video"], output: ["text"] },
},
);

expect(model.modalities?.input).toEqual(["text", "image", "video"]);
expect(model.attachment).toBe(true);
});

test("uses Fireworks context length only as an upper bound", () => {
const model = buildFireworksModel(
fireworksModel({ context_length: 131_072 }),
existingModel(),
);

expect(model.limit?.context).toBe(131_072);
expect(model.limit?.output).toBe(131_072);
});

test("does not report Fireworks embedding rows as missing generation models", () => {
const embedding = fireworksModel({ kind: "EMBEDDING_MODEL" });

expect(fireworksAi.sourceID(embedding)).toBeUndefined();
expect(fireworksAi.translateModel(embedding, {
existing: () => existingModel(),
authored: () => existingModel(),
})).toBeUndefined();
});

function fireworksModel(overrides: Partial<FireworksModel> = {}): FireworksModel {
return {
id: "accounts/fireworks/models/example",
object: "model",
created: 1_788_566_400,
owned_by: "fireworks",
context_length: 1_048_576,
kind: "HF_BASE_MODEL",
supports_chat: true,
supports_image_input: false,
supports_tools: true,
...overrides,
};
}

function existingModel(): ExistingModel {
return {
name: "Example",
description: "Example reasoning model",
release_date: "2026-09-01",
last_updated: "2026-09-01",
attachment: false,
reasoning: true,
reasoning_options: [{ type: "effort", values: ["low", "high"] }],
temperature: true,
tool_call: true,
structured_output: true,
open_weights: true,
cost: { input: 1, output: 2 },
limit: { context: 1_048_573, output: 262_144 },
modalities: { input: ["text"], output: ["text"] },
};
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
base_model = "alibaba/qwen3.8-max"

attachment = false

[[reasoning_options]]
type = "toggle"

[modalities]
input = ["text"]
output = ["text"]

[cost]
input = 2.00
output = 6.00
input = 2
output = 6
cache_read = 0.25

[limit]
context = 262_144

[modalities]
input = ["text", "image"]
13 changes: 13 additions & 0 deletions sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,19 @@ OVHcloud AI Endpoints is implemented in `packages/core/src/sync/providers/ovhclo
- `attachment` is derived from non-text `input_modalities`, and `open_weights` from the presence of `hugging_face_id`.
- `release_date`/`last_updated` default to the catalog `created` timestamp but preserve any existing hand-authored dates; `knowledge`, `family`, `status`, `interleaved`, and `limit.input` are preserved when present.

## Fireworks AI Notes

Fireworks AI is implemented in `packages/core/src/sync/providers/fireworks-ai.ts`.

- Run it with `bun models:sync fireworks-ai` or `bun fireworks:sync`.
- Source endpoint: `https://api.fireworks.ai/inference/v1/models`; required auth: `FIREWORKS_API_KEY`.
- The OpenAI-compatible response is the most complete callable catalog for an account: it includes base-model and router IDs, creation timestamps, advertised context length, model kind, and chat/image/tool capability flags.
- The endpoint does not expose OpenRouter-format metadata. `format=openrouter`, `type=text`, `limit`, and `pageSize` are accepted but ignored and return the same unpaginated response.
- The management endpoint at `/v1/accounts/fireworks/models?filter=supports_serverless=true` exposes richer base-model metadata but omits routers and can omit callable models. Its documented `serverlessModes` pricing data was empty in the live response, so it is not used by the sync.
- New text/vision IDs are reported but not created automatically because neither endpoint provides pricing, output limits, reasoning controls, or structured-output metadata. Embedding/reranking rows are ignored because the catalog provider entries describe generation models.
- Existing exact context caps smaller than the advertised API value are preserved; a lower API ceiling is applied. Positive image/tool flags upgrade authored capabilities, but false flags do not remove them: `minimax-m3` reported `supports_image_input=false` while successfully accepting an image completion. Authored output limits, pricing, reasoning options, and other metadata are preserved.
- Models absent from this account-scoped response are retained for manual lifecycle review.

## DigitalOcean Notes

- DigitalOcean is implemented in `packages/core/src/sync/providers/digitalocean.ts`.
Expand Down
Loading