Skip to content

Commit ff7eddc

Browse files
feat: add Ambient provider with GLM-5.1 and Kimi K2.6
Adds the Ambient inference provider (api.ambient.xyz) with an initial catalog of GLM-5.1 and Kimi K2.6, plus a generator script that pulls from /v1/models so pricing and limits stay in sync with the upstream API. Run `bun run ambient:generate` to refresh model TOMLs.
1 parent 1c2546a commit ff7eddc

6 files changed

Lines changed: 214 additions & 1 deletion

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"venice:generate": "bun ./packages/core/script/generate-venice.ts",
2424
"vercel:generate": "bun ./packages/core/script/generate-vercel.ts",
2525
"wandb:generate": "bun ./packages/core/script/generate-wandb.ts",
26-
"digitalocean:generate": "bun ./packages/core/script/generate-digitalocean.ts"
26+
"digitalocean:generate": "bun ./packages/core/script/generate-digitalocean.ts",
27+
"ambient:generate": "bun ./packages/core/script/generate-ambient.ts"
2728
},
2829
"dependencies": {
2930
"@cloudflare/workers-types": "^4.20260424.1",
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#!/usr/bin/env bun
2+
3+
/**
4+
* Generates Ambient model TOML files from https://api.ambient.xyz/v1/models.
5+
*
6+
* Emits `[extends]`-format TOMLs that inherit upstream metadata
7+
* (family, release_date, knowledge, capabilities) from the canonical
8+
* provider model, and override only the fields Ambient's API reports:
9+
* cost, limit, modalities.
10+
*
11+
* Flags:
12+
* --dry-run Preview generated TOMLs without writing files.
13+
*/
14+
15+
import { z } from "zod";
16+
import path from "node:path";
17+
import { mkdir } from "node:fs/promises";
18+
19+
const API_ENDPOINT = "https://api.ambient.xyz/v1/models";
20+
21+
// Allowlist for the initial rollout.
22+
const ALLOWLIST = new Set<string>([
23+
"zai-org/GLM-5.1-FP8",
24+
"moonshotai/kimi-k2.6",
25+
]);
26+
27+
// Maps Ambient model IDs to canonical <provider>/<model> in this repo.
28+
// The generated TOML uses this path as `[extends].from` so capabilities
29+
// and metadata propagate from the upstream provider automatically.
30+
const EXTENDS_MAP: Record<string, string> = {
31+
"zai-org/GLM-5.1-FP8": "zai/glm-5.1",
32+
"moonshotai/kimi-k2.6": "moonshotai/kimi-k2.6",
33+
};
34+
35+
const Pricing = z
36+
.object({
37+
prompt: z.string(),
38+
completion: z.string(),
39+
input_cache_read: z.string().optional(),
40+
input_cache_write: z.string().optional(),
41+
})
42+
.passthrough();
43+
44+
const AmbientModel = z
45+
.object({
46+
id: z.string(),
47+
name: z.string(),
48+
context_length: z.number(),
49+
max_output_length: z.number(),
50+
input_modalities: z.array(z.string()),
51+
output_modalities: z.array(z.string()),
52+
pricing: Pricing,
53+
})
54+
.passthrough();
55+
56+
const AmbientResponse = z
57+
.object({
58+
object: z.literal("list"),
59+
data: z.array(AmbientModel),
60+
})
61+
.passthrough();
62+
63+
const ALLOWED_MODALITIES = new Set(["text", "audio", "image", "video", "pdf"]);
64+
65+
function modalities(values: string[]): string[] {
66+
return values
67+
.map((v) => v.toLowerCase())
68+
.filter((v) => ALLOWED_MODALITIES.has(v));
69+
}
70+
71+
function perMTok(price: string): number {
72+
const n = parseFloat(price);
73+
if (!Number.isFinite(n)) {
74+
throw new Error(`Invalid price: ${price}`);
75+
}
76+
// Round to 6 decimals to absorb float noise from per-token strings.
77+
return Math.round(n * 1_000_000 * 1_000_000) / 1_000_000;
78+
}
79+
80+
function formatToml(
81+
model: z.infer<typeof AmbientModel>,
82+
extendsFrom: string,
83+
): string {
84+
const lines: string[] = [];
85+
lines.push("[extends]");
86+
lines.push(`from = "${extendsFrom}"`);
87+
lines.push("");
88+
89+
lines.push("[cost]");
90+
lines.push(`input = ${perMTok(model.pricing.prompt)}`);
91+
lines.push(`output = ${perMTok(model.pricing.completion)}`);
92+
if (model.pricing.input_cache_read !== undefined) {
93+
lines.push(`cache_read = ${perMTok(model.pricing.input_cache_read)}`);
94+
}
95+
if (model.pricing.input_cache_write !== undefined) {
96+
lines.push(`cache_write = ${perMTok(model.pricing.input_cache_write)}`);
97+
}
98+
lines.push("");
99+
100+
lines.push("[limit]");
101+
lines.push(`context = ${model.context_length}`);
102+
lines.push(`output = ${model.max_output_length}`);
103+
lines.push("");
104+
105+
const input = modalities(model.input_modalities);
106+
const output = modalities(model.output_modalities);
107+
lines.push("[modalities]");
108+
lines.push(`input = [${input.map((m) => `"${m}"`).join(", ")}]`);
109+
lines.push(`output = [${output.map((m) => `"${m}"`).join(", ")}]`);
110+
111+
return lines.join("\n") + "\n";
112+
}
113+
114+
async function main() {
115+
const dryRun = process.argv.includes("--dry-run");
116+
117+
const outDir = path.join(
118+
import.meta.dirname,
119+
"..",
120+
"..",
121+
"..",
122+
"providers",
123+
"ambient",
124+
"models",
125+
);
126+
127+
const res = await fetch(API_ENDPOINT);
128+
if (!res.ok) {
129+
console.error(`Fetch failed: ${res.status} ${res.statusText}`);
130+
process.exit(1);
131+
}
132+
133+
const parsed = AmbientResponse.safeParse(await res.json());
134+
if (!parsed.success) {
135+
console.error("Invalid Ambient response:", parsed.error.issues);
136+
process.exit(1);
137+
}
138+
139+
const selected = parsed.data.data.filter((m) => ALLOWLIST.has(m.id));
140+
const missing = [...ALLOWLIST].filter(
141+
(id) => !selected.some((m) => m.id === id),
142+
);
143+
if (missing.length > 0) {
144+
console.error(`Allowlisted models missing from API: ${missing.join(", ")}`);
145+
process.exit(1);
146+
}
147+
148+
let count = 0;
149+
for (const model of selected) {
150+
const extendsFrom = EXTENDS_MAP[model.id];
151+
if (!extendsFrom) {
152+
console.error(`No EXTENDS_MAP entry for ${model.id}; skipping`);
153+
continue;
154+
}
155+
const filePath = path.join(outDir, `${model.id}.toml`);
156+
const toml = formatToml(model, extendsFrom);
157+
if (dryRun) {
158+
console.log(`--- ${path.relative(process.cwd(), filePath)} ---`);
159+
console.log(toml);
160+
} else {
161+
await mkdir(path.dirname(filePath), { recursive: true });
162+
await Bun.write(filePath, toml);
163+
}
164+
count++;
165+
}
166+
167+
console.log(
168+
`${dryRun ? "Previewed" : "Wrote"} ${count} model file(s) under providers/ambient/models/`,
169+
);
170+
}
171+
172+
await main();

providers/ambient/logo.svg

Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[extends]
2+
from = "moonshotai/kimi-k2.6"
3+
4+
[cost]
5+
input = 0.95
6+
output = 4
7+
cache_read = 0.2
8+
cache_write = 0
9+
10+
[limit]
11+
context = 262144
12+
output = 262144
13+
14+
[modalities]
15+
input = ["text", "image"]
16+
output = ["text"]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[extends]
2+
from = "zai/glm-5.1"
3+
4+
[cost]
5+
input = 1.4
6+
output = 4.4
7+
cache_read = 0
8+
cache_write = 0
9+
10+
[limit]
11+
context = 202752
12+
output = 131072
13+
14+
[modalities]
15+
input = ["text"]
16+
output = ["text"]

providers/ambient/provider.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
name = "Ambient"
2+
env = ["AMBIENT_API_KEY"]
3+
npm = "@ai-sdk/openai-compatible"
4+
api = "https://api.ambient.xyz/v1"
5+
doc = "https://ambient.xyz"

0 commit comments

Comments
 (0)