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
59 changes: 55 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
"@vercel/functions": "^3.1.4",
"@xyflow/react": "^12.8.6",
"ai": "^6.0.72",
"aieo": "^0.1.34",
"aieo": "^0.1.35",
"autoprefixer": "^10.4.21",
"axios": "1.10.0",
"bcryptjs": "^3.0.2",
Expand Down
37 changes: 37 additions & 0 deletions src/__tests__/unit/lib/ai/models-aieo-parity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, test, expect, afterEach } from "vitest";
import { PROVIDERS, hasApiKeyForProvider } from "aieo";
import { PROVIDER_API_KEY_ENV_VARS, PROVIDER_DISPLAY_LABELS } from "@/lib/ai/models";

/**
* `src/lib/ai/models.ts` can't import aieo (it's pulled into client
* bundles), so it carries its own provider -> env-var table. This test
* pins that table to aieo's key lookup so a provider added to one side
* (e.g. xai) can't silently be missing or mis-keyed on the other.
*/
describe("models.ts provider table matches aieo", () => {
const saved: Record<string, string | undefined> = {};

afterEach(() => {
for (const [k, v] of Object.entries(saved)) {
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
});

test.each(PROVIDERS)("aieo provider %s has a hive env var and display label", (provider) => {
const enumKey = provider.toUpperCase();
expect(PROVIDER_API_KEY_ENV_VARS[enumKey]).toBeTruthy();
expect(PROVIDER_DISPLAY_LABELS[enumKey]).toBeTruthy();
});

test.each(PROVIDERS)("setting hive's env var for %s satisfies aieo's key lookup", (provider) => {
const envVar = PROVIDER_API_KEY_ENV_VARS[provider.toUpperCase()]!;
saved[envVar] = process.env[envVar];

delete process.env[envVar];
expect(hasApiKeyForProvider(provider)).toBe(false);

process.env[envVar] = "parity-test-key";
expect(hasApiKeyForProvider(provider)).toBe(true);
});
});
117 changes: 0 additions & 117 deletions src/__tests__/unit/lib/ai/runCanvasAgent-xai.test.ts

This file was deleted.

16 changes: 8 additions & 8 deletions src/app/api/agent/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,14 +362,14 @@ async function createAgentSession(
}
}

// xAI bypass: the Bifrost VK reconciler falls back to anthropic's
// provider suffix for any model prefix it doesn't recognize, and its
// provider allow-list doesn't include xai — routing an xai/* session
// through Bifrost today would mint a VK pointed at the wrong
// provider. Skip the call for xai/* and use the direct XAI_API_KEY
// resolved above instead. Remove once an `aieo` gateway path + a
// Bifrost-side xai provider config both exist (see
// src/services/task-workflow.ts for the matching bypass).
// xAI bypass: the Bifrost VK provider allow-list (DEFAULT_PROVIDERS in
// src/services/bifrost/constants.ts) doesn't include xai, and the
// swarm gateways have no xai provider key configured — routing an
// xai/* session through Bifrost today would fail. Skip the call for
// xai/* and use the direct XAI_API_KEY resolved above instead. Remove
// once the gateways carry an xai key and DEFAULT_PROVIDERS lists it
// (aieo already maps xai onto the /openai/v1 gateway path). See
// src/services/task-workflow.ts for the matching bypass.
const isXaiModel = effectiveModel?.startsWith("xai/") ?? false;

// Bifrost routing for the goose-side LLM calls. When the rollout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,14 @@ export function CanvasAgentSettingsPopover({
};
}, []);

// Load the available models for the picker. xAI/Grok rows are excluded
// here specifically (not just left to the server-side key filter) —
// `aieo` (the canvas agent's LLM SDK, ^0.1.34) has no xai provider
// entry yet, so `runCanvasAgent` throws a visible error rather than
// silently answering as Anthropic if one is selected. Hiding it from
// this picker means users never hit that error in the first place.
// Remove this filter once aieo supports xai — see the "Add xAI"
// feature notes.
// Load the available models for the picker. /api/llm-models already
// filters out providers whose API key isn't configured server-side.
useEffect(() => {
let cancelled = false;
fetch("/api/llm-models")
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (!cancelled && data?.models) {
setModels((data.models as LlmModelOption[]).filter((m) => m.provider !== "XAI"));
}
if (!cancelled && data?.models) setModels(data.models);
})
.catch(() => {
/* leave empty; picker stays hidden until a retry */
Expand Down
2 changes: 1 addition & 1 deletion src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export const optionalEnvVars = {
// raw `process.env` read — so it's a typed, discoverable config value like
// its sibling provider keys. Absence is a normal, expected state in any
// environment that hasn't onboarded xAI yet: `/api/llm-models` filters out
// XAI rows when this is unset (see step 8 of the xAI feature), so pickers
// XAI rows when this is unset, so pickers
// stay empty rather than erroring.
XAI_API_KEY: process.env.XAI_API_KEY || "",
} as const;
Expand Down
12 changes: 8 additions & 4 deletions src/lib/ai/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,20 @@ export function isValidModel(model: unknown): model is string {
return false;
}

// Map LlmProvider enum values to their API key environment variables
// Map LlmProvider enum values to their API key environment variables.
//
// For the providers aieo knows about (anthropic/google/openai/openrouter/xai)
// these MUST match the env vars aieo's own key lookup reads — aieo is the
// source of truth for provider routing, and `runCanvasAgent` resolves keys
// through it directly. This file is imported by client components, so it
// can't import aieo itself; `models-aieo-parity.test.ts` guards the two
// tables against drifting apart. AWS_BEDROCK/OTHER are hive-only.
export const PROVIDER_API_KEY_ENV_VARS: Record<string, string | null> = {
ANTHROPIC: "ANTHROPIC_API_KEY",
OPENAI: "OPENAI_API_KEY",
GOOGLE: "GOOGLE_API_KEY",
AWS_BEDROCK: "AWS_BEDROCK_API_KEY",
OPENROUTER: "OPENROUTER_API_KEY",
// Direct xAI credential — Grok models resolve to `xai/<name>` and use
// this key instead of routing through OpenRouter. See the "Add xAI"
// feature notes for the Bifrost/canvas-agent gaps this doesn't cover yet.
XAI: "XAI_API_KEY",
OTHER: null,
};
Expand Down
17 changes: 1 addition & 16 deletions src/lib/ai/runCanvasAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,22 +697,7 @@ export async function runCanvasAgent(
// flag is on for the primary workspace — see `getBifrostForLLM` below.
let provider: Provider = "anthropic";
if (modelName?.includes("/")) {
const rawPrefix = modelName.split("/")[0];
// aieo (^0.1.34, the pinned version) has no "xai" entry in `PROVIDERS`
// — it's not just an unconfigured key, the provider itself doesn't
// exist on this path yet. Falling through to the generic branch
// below would silently answer a Grok selection as Anthropic, which
// is exactly the failure mode this feature must not have. Fail
// loudly instead. `CanvasAgentSettingsPopover` excludes xai/* rows
// from the picker, so this should only fire on a stale/tampered
// `chatAgentModel` preference. Remove this guard once `aieo` (or its
// replacement) supports xai — see the "Add xAI" feature notes.
if (rawPrefix === "xai") {
throw new Error(
`runCanvasAgent: model "${modelName}" is not available on this path — xAI/Grok is not yet supported by the canvas agent's LLM SDK (aieo). Choose a different model.`,
);
}
const prefix = rawPrefix as Provider;
const prefix = modelName.split("/")[0] as Provider;
if (!PROVIDERS.includes(prefix)) {
console.warn(
`[runCanvasAgent] model "${modelName}" has unsupported provider prefix "${prefix}"; falling back to anthropic default`,
Expand Down
17 changes: 8 additions & 9 deletions src/services/task-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,15 +853,14 @@ export async function callStakworkAPI(params: {
// tuning of ttlSeconds / maxCostUsd / maxSteps is intentionally
// deferred to a follow-up so this initial wiring stays small.
//
// xAI bypass: `reconcileBifrostVK` derives the `baseUrl` provider
// suffix from the model prefix and falls back to anthropic for any
// prefix it doesn't recognize, and `DEFAULT_PROVIDERS` doesn't list
// "xai" — so routing an `xai/*` selection through Bifrost today would
// mint a VK pointed at the wrong provider (or error). Skip the
// Bifrost call entirely for xai/* and fall through to the direct
// `vars.apiKey` (XAI_API_KEY) resolved above. Remove this bypass once
// an `aieo` version with an xAI gateway path AND a Bifrost-side xAI
// provider config both exist — until then this trades away per-agent
// xAI bypass: the Bifrost VK provider allow-list (`DEFAULT_PROVIDERS`)
// doesn't list "xai" and the swarm gateways have no xai provider key
// configured, so routing an `xai/*` selection through Bifrost today
// would fail. Skip the Bifrost call entirely for xai/* and fall
// through to the direct `vars.apiKey` (XAI_API_KEY) resolved above.
// Remove this bypass once the gateways carry an xai key and
// `DEFAULT_PROVIDERS` lists it (aieo already maps xai onto the
// /openai/v1 gateway path) — until then this trades away per-agent
// cost attribution / macaroon observability for Grok runs only.
const isXaiModel = effectiveModel?.startsWith("xai/") ?? false;
const bifrost = isXaiModel
Expand Down
Loading