Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/contributing/billing.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ BYOK_OPENROUTER_API_KEY=sk-or-v1-... # https://openrouter.ai/keys

- Bypasses **billing only — never auth.** Still sign in (`insider@grida.co` / `password`); a resolvable org is still required (an unauthenticated request still 401s).
- **Text/chat only** — BYOK swaps the AI-SDK provider, so only the text path is unbilled. Image/audio go through Replicate (`withTransaction`) and **still gate + bill even under BYOK** — those features need the full billing setup. (OpenRouter also exposes no image/audio models.) Catalog model IDs are unchanged; use IDs your provider accepts (edit `editor/lib/ai/models.ts` locally if one 404s).
- Precedence if both are set: OpenRouter, then AI Gateway. Fail-closed — an empty/unset (or whitespace-only) key falls back to the billed path.
- Precedence if both are set: OpenRouter, then Vercel. Fail-closed — an empty/unset (or whitespace-only) key falls back to the billed path.
- **Never set `BYOK_*` on a hosted or preview deploy.** It disables billing **and** the org-id sanity gate for every org. Contributor / self-host / local only. See [SECURITY.md](https://github.com/gridaco/grida/blob/main/SECURITY.md) (`GRIDA-SEC-003`, BYOK carve-out).

Working on billing itself? Ignore BYOK and continue with the full setup below.
Expand Down
6 changes: 3 additions & 3 deletions docs/wg/ai/grida/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ There is intentionally no `/secrets/get`, no `/auth/*`, and no
V1 provider resolution is BYOK-only:

1. `openrouter`
2. `ai-gateway`
2. `vercel`
3. unavailable (`provider_down`)

`AgentRunOptions.providerId` accepts only `ByokProviderId`. The package root
Expand Down Expand Up @@ -215,8 +215,8 @@ describe("handshake", () => {
});

describe("provider resolution", () => {
it("prefers OpenRouter BYOK over AI Gateway BYOK");
it("falls back to AI Gateway BYOK");
it("prefers OpenRouter BYOK over Vercel BYOK");
it("falls back to Vercel BYOK");
it("throws provider_down when no BYOK key is present");
it("validates explicit BYOK provider ids");
});
Expand Down
2 changes: 1 addition & 1 deletion editor/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ OPENAI_API_KEY='sk-xxx'
# route through that provider and BYPASS the billing layer (no credit
# gate, no metering). does NOT bypass auth (login + org still required).
# text/chat only; catalog model IDs unchanged (use IDs the provider
# accepts). precedence: openrouter, then ai-gateway.
# accepts). precedence: openrouter, then vercel.
# BYOK_OPENROUTER_API_KEY=sk-or-v1-xxx # https://openrouter.ai/keys
# BYOK_AI_GATEWAY_API_KEY=... # dedicated Vercel AI Gateway key

Expand Down
2 changes: 1 addition & 1 deletion editor/lib/ai/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export const gateway = createGateway({
// active only when a key env var is a non-empty string after trim
// (whitespace-only secrets fall back to the billed path).
//
// Implementations (precedence: OpenRouter first, then AI Gateway). A
// Implementations (precedence: OpenRouter first, then Vercel). A
// third BYOK key is a new branch here — no registry.
// ---------------------------------------------------------------------------
function resolveByokProvider() {
Expand Down
2 changes: 1 addition & 1 deletion editor/lib/desktop/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("desktop bridge client contract", () => {

it("uses producer-owned BYOK provider metadata for settings", () => {
expect(secrets.byokProviderMetadata()).toBe(BYOK_PROVIDER_METADATA);
expect(secrets.byokProviders()).toEqual(["openrouter", "ai-gateway"]);
expect(secrets.byokProviders()).toEqual(["openrouter", "vercel"]);
});

it("rejects empty keys before calling the bridge", async () => {
Expand Down
8 changes: 4 additions & 4 deletions packages/grida-ai-agent/src/__public-api__.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,14 @@ describe("@grida/agent public API", () => {
});

it("exposes BYOK provider identity, wire vocab, tiers, and session-row types", () => {
expect(BYOK_PROVIDER_IDS).toEqual(["openrouter", "ai-gateway"]);
expect(BYOK_PROVIDER_IDS).toEqual(["openrouter", "vercel"]);
expect(BYOK_PROVIDER_METADATA.map((provider) => provider.label)).toEqual([
"OpenRouter",
"AI Gateway",
"Vercel",
]);
const byok: ByokProviderId = "ai-gateway";
const byok: ByokProviderId = "vercel";
const metadata: ByokProviderMetadata = BYOK_PROVIDER_METADATA[0];
expect(byok).toBe("ai-gateway");
expect(byok).toBe("vercel");
expect(metadata.id).toBe("openrouter");

// Tier constants.
Expand Down
6 changes: 3 additions & 3 deletions packages/grida-ai-agent/src/auth/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
*
* ```json
* {
* "openrouter": { "type": "api", "key": "sk-or-..." },
* "ai-gateway": { "type": "api", "key": "..." }
* "openrouter": { "type": "api", "key": "sk-or-..." },
* "vercel": { "type": "api", "key": "..." }
* }
* ```
*
Expand Down Expand Up @@ -97,7 +97,7 @@ export class AuthStore {
* Serializes `set` / `remove` so two concurrent mutations can't
* lose-update each other. Both ops follow read-modify-write on a
* shared file; without this chain a parallel set('openrouter', …)
* and set('ai-gateway', …) would both `readAll` the same starting
* and set('vercel', …) would both `readAll` the same starting
* state, both `writeAll`, and the second `rename` wins — silently
* dropping the first key. The `.catch(() => undefined)` on the
* chain swallows rejection so a single failed write doesn't strand
Expand Down
2 changes: 1 addition & 1 deletion packages/grida-ai-agent/src/http/routes/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*
* Allowed provider ids — a closed set:
* - `openrouter`
* - `ai-gateway`
* - `vercel`
*
* Any other id is rejected with a 400 so a typo doesn't silently create a
* never-used auth.json entry.
Expand Down
4 changes: 2 additions & 2 deletions packages/grida-ai-agent/src/protocol/provider-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export const BYOK_PROVIDER_METADATA = [
label: "OpenRouter",
},
{
id: "ai-gateway",
label: "AI Gateway",
id: "vercel",
label: "Vercel",
},
] as const;

Expand Down
2 changes: 1 addition & 1 deletion packages/grida-ai-agent/src/providers/byok.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function makeOpenRouterFactory(apiKey: string): ModelFactory {
return (tier, modelId) => provider(modelId ?? MODEL_BY_TIER[tier]);
}

export function makeAiGatewayFactory(apiKey: string): ModelFactory {
export function makeVercelFactory(apiKey: string): ModelFactory {
const provider = createGateway({ apiKey });
return (tier, modelId) => provider(modelId ?? MODEL_BY_TIER[tier]);
}
16 changes: 7 additions & 9 deletions packages/grida-ai-agent/src/providers/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ function deps(keys: Record<string, string | null> = {}) {
}

describe("resolveProvider", () => {
it("prefers OpenRouter over AI Gateway when both BYOK keys exist", async () => {
it("prefers OpenRouter over Vercel when both BYOK keys exist", async () => {
const provider = await resolveProvider(
deps({
openrouter: " sk-or ",
"ai-gateway": "ai-gateway-key",
vercel: "vercel-key",
})
);

Expand All @@ -28,12 +28,10 @@ describe("resolveProvider", () => {
expect(provider.model_factory).toBeTypeOf("function");
});

it("falls back to AI Gateway when OpenRouter is absent", async () => {
const provider = await resolveProvider(
deps({ "ai-gateway": "ai-gateway-key" })
);
it("falls back to Vercel when OpenRouter is absent", async () => {
const provider = await resolveProvider(deps({ vercel: "vercel-key" }));

expect(provider.provider_id).toBe("ai-gateway");
expect(provider.provider_id).toBe("vercel");
expect(provider.kind).toBe("byok");
});

Expand All @@ -46,9 +44,9 @@ describe("resolveProvider", () => {
it("throws when an explicit BYOK provider has no key", async () => {
await expect(
resolveProvider(deps({ openrouter: "sk-or" }), {
explicit: "ai-gateway",
explicit: "vercel",
})
).rejects.toMatchObject({ provider_id: "ai-gateway" });
).rejects.toMatchObject({ provider_id: "vercel" });
});

it("BYOK factory honors an explicit modelId over the tier model", async () => {
Expand Down
8 changes: 4 additions & 4 deletions packages/grida-ai-agent/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* the factory, so it's cheap on the hot path and easy to test.
*
* This is the providers layer, not a generic model-provider router. V1 is
* BYOK-only: OpenRouter takes precedence over AI Gateway, and a missing
* BYOK-only: OpenRouter takes precedence over Vercel, and a missing
* key throws `ProviderUnavailableError`.
*/

Expand All @@ -20,7 +20,7 @@ import {
BYOK_PROVIDER_METADATA,
type ByokProviderId,
} from "../protocol/provider-ids";
import { makeAiGatewayFactory, makeOpenRouterFactory } from "./byok";
import { makeOpenRouterFactory, makeVercelFactory } from "./byok";

/** Canonical tier->catalog-model map. One table, sourced from @grida/ai-models. */
export const MODEL_BY_TIER: Record<ModelTier, TierModelId> = TIER_MODEL_IDS;
Expand Down Expand Up @@ -98,11 +98,11 @@ function makeResolvedProvider(
kind: "byok",
model_factory: makeOpenRouterFactory(key.trim()),
};
case "ai-gateway":
case "vercel":
return {
provider_id: providerId,
kind: "byok",
model_factory: makeAiGatewayFactory(key.trim()),
model_factory: makeVercelFactory(key.trim()),
};
}
const _exhaustive: never = providerId;
Expand Down
2 changes: 1 addition & 1 deletion packages/grida-ai-agent/src/sandbox/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export type AgentHostSandboxPolicy = {

const BYOK_PROVIDER_NETWORK_HOSTS = {
openrouter: ["openrouter.ai"],
"ai-gateway": ["ai-gateway.vercel.sh", "*.vercel-ai.com"],
vercel: ["ai-gateway.vercel.sh", "*.vercel-ai.com"],
} as const satisfies Record<ByokProviderId, readonly string[]>;

const ALWAYS_ALLOWED_HOSTS: readonly string[] = Object.values(
Expand Down
2 changes: 1 addition & 1 deletion packages/grida-ai-agent/src/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* GRIDA-SEC-004 — BYOK secret store.
*
* Thin façade over `AuthStore` for the `ApiKeyEntry`-shaped records
* (`openrouter`, `ai-gateway`). The agent host's `/secrets/*` HTTP routes
* (`openrouter`, `vercel`). The agent host's `/secrets/*` HTTP routes
* call this; the BYOK provider path in `runtime.ts`
* calls this internally to pull the key when constructing the
* @ai-sdk client.
Expand Down
4 changes: 2 additions & 2 deletions packages/grida-ai-models/__tests__/models.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import models, { TIER_MODEL_IDS } from "..";

describe("models.image.findImageModelCard", () => {
it("resolves a full gateway id", () => {
it("resolves a full vercel id", () => {
const card = models.image.findImageModelCard("bfl/flux-pro-1.1");
expect(card?.id).toBe("bfl/flux-pro-1.1");
expect(card?.label).toBe("Flux Pro 1.1");
});

it("resolves the deprecated ProviderModel wrapper", () => {
const card = models.image.findImageModelCard({
provider: "gateway",
provider: "vercel",
modelId: "bfl/flux-2-pro",
});
expect(card?.id).toBe("bfl/flux-2-pro");
Expand Down
30 changes: 15 additions & 15 deletions packages/grida-ai-models/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* modules — keeping the full `namespace models` declaration in a
* single source file is the workaround.
*
* `provider: "gateway"` and `provider: "replicate"` on the cards are
* `provider: "vercel"` and `provider: "replicate"` on the cards are
* data labels only — see the README for the full contract.
*
* @module
Expand All @@ -31,11 +31,11 @@ export namespace models {
// ── Shared discriminators ─────────────────────────────────────────

/**
* Routing label for hosted-provider calls. `"gateway"` indicates
* the model is served via a hosted AI gateway (e.g. Vercel AI
* Gateway); the label is data, not an SDK directive.
* Routing label for hosted-provider calls. `"vercel"` indicates
* the model is served via the Vercel AI Gateway; the label is
* data, not an SDK directive.
*/
export type Provider = "gateway";
export type Provider = "vercel";

/**
* Model vendor (the organization that produced the weights).
Expand Down Expand Up @@ -226,7 +226,7 @@ export namespace models {
* a `provider` field on its own.
*/
export type ProviderModel = {
provider: "gateway";
provider: "vercel";
modelId: ImageModelId;
};

Expand Down Expand Up @@ -461,7 +461,7 @@ export namespace models {
short_description:
"State-of-the-art image generation and editing with flexible resolutions",
vendor: "openai",
provider: "gateway",
provider: "vercel",
speed_label: "medium",
speed_max: "1m",
styles: null,
Expand Down Expand Up @@ -514,7 +514,7 @@ export namespace models {
short_description:
"Previous-generation image model. Superseded by GPT Image 2.",
vendor: "openai",
provider: "gateway",
provider: "vercel",
speed_label: "medium",
speed_max: "1m",
styles: null,
Expand Down Expand Up @@ -561,7 +561,7 @@ export namespace models {
deprecated: false,
short_description: "Cost-efficient image generation model",
vendor: "openai",
provider: "gateway",
provider: "vercel",
speed_label: "slow",
speed_max: "1m",
styles: null,
Expand Down Expand Up @@ -613,7 +613,7 @@ export namespace models {
short_description:
"Fast, efficient multimodal model with native image generation",
vendor: "google",
provider: "gateway",
provider: "vercel",
speed_label: "fast",
speed_max: "15s",
styles: null,
Expand All @@ -636,7 +636,7 @@ export namespace models {
short_description:
"High-quality multimodal model with native image generation",
vendor: "google",
provider: "gateway",
provider: "vercel",
speed_label: "medium",
speed_max: "30s",
styles: null,
Expand All @@ -662,7 +662,7 @@ export namespace models {
short_description:
"Latest Flux model with best-in-class image quality and prompt adherence",
vendor: "black-forest-labs",
provider: "gateway",
provider: "vercel",
speed_label: "medium",
speed_max: "30s",
styles: null,
Expand All @@ -683,7 +683,7 @@ export namespace models {
short_description:
"Highest quality Flux model for context-aware image generation and editing",
vendor: "black-forest-labs",
provider: "gateway",
provider: "vercel",
speed_label: "slow",
speed_max: "30s",
styles: null,
Expand All @@ -703,7 +703,7 @@ export namespace models {
deprecated: false,
short_description: "Fast context-aware image generation and editing",
vendor: "black-forest-labs",
provider: "gateway",
provider: "vercel",
speed_label: "medium",
speed_max: "20s",
styles: null,
Expand All @@ -724,7 +724,7 @@ export namespace models {
short_description:
"Faster, better FLUX Pro. Text-to-image model with excellent image quality and output diversity.",
vendor: "black-forest-labs",
provider: "gateway",
provider: "vercel",
speed_label: "slow",
speed_max: "30s",
styles: null,
Expand Down
Loading