Skip to content
Open
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
3 changes: 2 additions & 1 deletion lib/agents/CompactAgent/createCompactAgent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ToolLoopAgent, stepCountIs } from "ai";
import { LIGHTWEIGHT_MODEL } from "@/lib/const";
import { createModel } from "@/lib/ai/createModel";

const DEFAULT_INSTRUCTIONS = `You are a conversation summarizer. Create a concise summary of the conversation that:
- Preserves key information, decisions, and action items
Expand All @@ -17,7 +18,7 @@ Respond with only the summary text, no additional commentary.`;
*/
export function createCompactAgent(customInstructions?: string) {
return new ToolLoopAgent({
model: LIGHTWEIGHT_MODEL,
model: createModel(LIGHTWEIGHT_MODEL),
instructions: customInstructions || DEFAULT_INSTRUCTIONS,
stopWhen: stepCountIs(1),
});
Expand Down
3 changes: 2 additions & 1 deletion lib/agents/EmailReplyAgent/createEmailReplyAgent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Output, ToolLoopAgent, stepCountIs } from "ai";
import { z } from "zod";
import { LIGHTWEIGHT_MODEL, INBOUND_EMAIL_DOMAIN } from "@/lib/const";
import { createModel } from "@/lib/ai/createModel";

const replyDecisionSchema = z.object({
shouldReply: z.boolean().describe("Whether the Recoup AI assistant should reply to this email"),
Expand All @@ -22,7 +23,7 @@ Rules (check in this order):
*/
export function createEmailReplyAgent() {
return new ToolLoopAgent({
model: LIGHTWEIGHT_MODEL,
model: createModel(LIGHTWEIGHT_MODEL),
instructions,
output: Output.object({ schema: replyDecisionSchema }),
stopWhen: stepCountIs(1),
Expand Down
3 changes: 2 additions & 1 deletion lib/agents/content/createContentPromptAgent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Output, ToolLoopAgent, stepCountIs } from "ai";
import { z } from "zod";
import { LIGHTWEIGHT_MODEL } from "@/lib/const";
import { createModel } from "@/lib/ai/createModel";
import { CONTENT_TEMPLATES, DEFAULT_CONTENT_TEMPLATE } from "@/lib/content/contentTemplates";
import { CAPTION_LENGTHS } from "@/lib/content/captionLengths";
import { songsSchema } from "@/lib/content/songsSchema";
Expand Down Expand Up @@ -68,7 +69,7 @@ Defaults: lipsync=${DEFAULT_CONTENT_PROMPT_FLAGS.lipsync}, batch=${DEFAULT_CONTE
*/
export function createContentPromptAgent() {
return new ToolLoopAgent({
model: LIGHTWEIGHT_MODEL,
model: createModel(LIGHTWEIGHT_MODEL),
instructions,
output: Output.object({ schema: contentPromptFlagsSchema }),
stopWhen: stepCountIs(1),
Expand Down
3 changes: 2 additions & 1 deletion lib/agents/generalAgent/getGeneralAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AnthropicProviderOptions } from "@ai-sdk/anthropic";
import { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google";
import { OpenAIResponsesProviderOptions } from "@ai-sdk/openai";
import { DEFAULT_MODEL } from "@/lib/const";
import { createModel } from "@/lib/ai/createModel";
import { RoutingDecision } from "@/lib/chat/types";
import { extractImageUrlsFromMessages } from "@/lib/messages/extractImageUrlsFromMessages";
import { buildSystemPromptWithImages } from "@/lib/chat/buildSystemPromptWithImages";
Expand Down Expand Up @@ -51,7 +52,7 @@ export default async function getGeneralAgent(body: ChatRequestBody): Promise<Ro
const instructions = buildSystemPromptWithImages(baseSystemPrompt, imageUrls);

const tools = await setupToolsForRequest(body);
const model = bodyModel || DEFAULT_MODEL;
const model = createModel(bodyModel || DEFAULT_MODEL);
const stopWhen = stepCountIs(111);

const agent = new ToolLoopAgent({
Expand Down
39 changes: 39 additions & 0 deletions lib/ai/createModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { LanguageModel } from "ai";
import { gateway } from "@ai-sdk/gateway";
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { openai } from "@ai-sdk/openai";

/**
* Resolves a model string (e.g. "openai/gpt-5-mini") to a LanguageModel,
* routing through whichever provider is configured.
*
* Priority: Vercel AI Gateway > OpenRouter > direct OpenAI.
*/
export function createModel(modelId: string): LanguageModel {
// Vercel AI Gateway — existing production behavior
if (process.env.VERCEL_AI_GATEWAY_API_KEY || process.env.VERCEL_OIDC_TOKEN) {
return gateway(modelId);
}

// OpenRouter — supports "provider/model" format natively
if (process.env.OPENROUTER_API_KEY) {
const openrouter = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY });
return openrouter(modelId);
}
Comment on lines +19 to +22
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KISS principle

  • Why add OpenRouter when we already support AI Gateway?


// Direct OpenAI — only supports openai/* models
if (process.env.OPENAI_API_KEY) {
if (!modelId.startsWith("openai/") && modelId.includes("/")) {
throw new Error(
`Model "${modelId}" is not an OpenAI model. Direct OpenAI mode only supports openai/* models. ` +
`Use OPENROUTER_API_KEY or VERCEL_AI_GATEWAY_API_KEY for multi-provider support.`,
);
}
const bareModel = modelId.startsWith("openai/") ? modelId.slice(7) : modelId;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return openai(bareModel);
}

throw new Error(
"No LLM provider configured. Set VERCEL_AI_GATEWAY_API_KEY, OPENROUTER_API_KEY, or OPENAI_API_KEY.",
);
}
3 changes: 2 additions & 1 deletion lib/ai/generateArray.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { generateObject } from "ai";
import { DEFAULT_MODEL } from "@/lib/const";
import { createModel } from "./createModel";
import { z } from "zod";

export interface GenerateArrayResult {
Expand All @@ -15,7 +16,7 @@ const generateArray = async ({
prompt: string;
}): Promise<GenerateArrayResult[]> => {
const result = await generateObject({
model: DEFAULT_MODEL,
model: createModel(DEFAULT_MODEL),
system,
prompt,
output: "array",
Expand Down
3 changes: 2 additions & 1 deletion lib/ai/generateText.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { generateText as generate } from "ai";
import { DEFAULT_MODEL } from "@/lib/const";
import { createModel } from "./createModel";

const generateText = async ({
system,
Expand All @@ -12,7 +13,7 @@ const generateText = async ({
}) => {
const result = await generate({
system,
model: model || DEFAULT_MODEL,
model: createModel(model || DEFAULT_MODEL),
prompt,
});

Expand Down
45 changes: 37 additions & 8 deletions lib/ai/getAvailableModels.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,45 @@
import { gateway, GatewayLanguageModelEntry } from "@ai-sdk/gateway";
import isEmbedModel from "./isEmbedModel";
import { DEFAULT_MODEL, LIGHTWEIGHT_MODEL } from "@/lib/const";

/**
* Returns the list of available LLMs from the Vercel AI Gateway.
* Filters out embed models that are not suitable for chat.
* Default model list for non-gateway providers (OpenRouter, direct OpenAI).
* Returned only when the Vercel AI Gateway is not configured.
*/
const DEFAULT_MODELS: GatewayLanguageModelEntry[] = [
{
id: DEFAULT_MODEL,
name: "GPT-5 Mini",
description: "Default model for chat and generation",
pricing: { input: "0.0001", output: "0.0004" },
specification: { specificationVersion: "v2", provider: "openai", modelId: DEFAULT_MODEL },
},
{
id: LIGHTWEIGHT_MODEL,
name: "GPT-4o Mini",
description: "Lightweight model for simple tasks",
pricing: { input: "0.00015", output: "0.0006" },
specification: { specificationVersion: "v2", provider: "openai", modelId: LIGHTWEIGHT_MODEL },
},
];

/**
* Returns the list of available LLMs.
* Uses Vercel AI Gateway when configured, otherwise returns a default list.
*/
export const getAvailableModels = async (): Promise<GatewayLanguageModelEntry[]> => {
try {
const apiResponse = await gateway.getAvailableModels();
const gatewayModels = apiResponse.models.filter(m => !isEmbedModel(m));
return gatewayModels;
} catch {
return [];
// Use Vercel AI Gateway when configured
if (process.env.VERCEL_AI_GATEWAY_API_KEY) {
try {
const apiResponse = await gateway.getAvailableModels();
const gatewayModels = apiResponse.models.filter(m => !isEmbedModel(m));
return gatewayModels;
} catch (error) {
console.error("[getAvailableModels] Gateway fetch failed:", error);
return [];
}
}

// Fallback for OpenRouter or direct OpenAI
return DEFAULT_MODELS;
};
3 changes: 2 additions & 1 deletion lib/catalog/analyzeCatalogBatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from "zod";
import { generateObject } from "ai";
import type { CatalogSongWithArtists } from "@/lib/supabase/catalog_songs/selectCatalogSongsWithArtists";
import { DEFAULT_MODEL } from "@/lib/const";
import { createModel } from "@/lib/ai/createModel";

/**
* Analyzes a single batch of catalog songs using AI to filter by criteria
Expand All @@ -17,7 +18,7 @@ export async function analyzeCatalogBatch(
): Promise<CatalogSongWithArtists[]> {
// Use AI to select relevant songs from this batch
const { object } = await generateObject({
model: DEFAULT_MODEL,
model: createModel(DEFAULT_MODEL),
schema: z.object({
selected_song_isrcs: z
.array(z.string())
Expand Down
33 changes: 17 additions & 16 deletions lib/chat/toolChains/toolChains.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LanguageModel, ModelMessage } from "ai";
import { createReleaseReportToolChain } from "./create_release_report/createReleaseReportToolChain";
import { createNewArtistToolChain } from "./createNewArtistToolChain";
import { createModel } from "@/lib/ai/createModel";

export type ToolChainItem = {
toolName: string;
Expand All @@ -18,22 +19,22 @@ export type PrepareStepResult = {
// Forced toolChoice is incompatible with Anthropic extended thinking.
// Every tool used in a chain must have a model here to avoid the conflict.
export const TOOL_MODEL_MAP: Record<string, LanguageModel> = {
update_account_info: "gemini-2.5-pro",
get_spotify_search: "openai/gpt-5.4-mini",
update_artist_socials: "openai/gpt-5.4-mini",
artist_deep_research: "openai/gpt-5.4-mini",
spotify_deep_research: "openai/gpt-5.4-mini",
get_artist_socials: "openai/gpt-5.4-mini",
get_spotify_artist_top_tracks: "openai/gpt-5.4-mini",
get_spotify_artist_albums: "openai/gpt-5.4-mini",
get_spotify_album: "openai/gpt-5.4-mini",
search_web: "openai/gpt-5.4-mini",
generate_txt_file: "openai/gpt-5.4-mini",
create_segments: "openai/gpt-5.4-mini",
youtube_login: "openai/gpt-5.4-mini",
web_deep_research: "openai/gpt-5.4-mini",
create_knowledge_base: "openai/gpt-5.4-mini",
send_email: "openai/gpt-5.4-mini",
update_account_info: createModel("google/gemini-2.5-pro"),
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When only OPENAI_API_KEY is set, createModel("google/gemini-2.5-pro") passes "google/gemini-2.5-pro" verbatim to the OpenAI SDK, which will reject it as an unknown model. The direct-OpenAI fallback in createModel only strips the openai/ prefix and has no mapping or error for non-OpenAI model IDs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/chat/toolChains/toolChains.ts, line 22:

<comment>When only `OPENAI_API_KEY` is set, `createModel("google/gemini-2.5-pro")` passes `"google/gemini-2.5-pro"` verbatim to the OpenAI SDK, which will reject it as an unknown model. The direct-OpenAI fallback in `createModel` only strips the `openai/` prefix and has no mapping or error for non-OpenAI model IDs.</comment>

<file context>
@@ -18,22 +19,22 @@ export type PrepareStepResult = {
-  web_deep_research: "openai/gpt-5.4-mini",
-  create_knowledge_base: "openai/gpt-5.4-mini",
-  send_email: "openai/gpt-5.4-mini",
+  update_account_info: createModel("google/gemini-2.5-pro"),
+  get_spotify_search: createModel("openai/gpt-5.4-mini"),
+  update_artist_socials: createModel("openai/gpt-5.4-mini"),
</file context>
Fix with Cubic

get_spotify_search: createModel("openai/gpt-5.4-mini"),
update_artist_socials: createModel("openai/gpt-5.4-mini"),
artist_deep_research: createModel("openai/gpt-5.4-mini"),
spotify_deep_research: createModel("openai/gpt-5.4-mini"),
get_artist_socials: createModel("openai/gpt-5.4-mini"),
get_spotify_artist_top_tracks: createModel("openai/gpt-5.4-mini"),
get_spotify_artist_albums: createModel("openai/gpt-5.4-mini"),
get_spotify_album: createModel("openai/gpt-5.4-mini"),
search_web: createModel("openai/gpt-5.4-mini"),
generate_txt_file: createModel("openai/gpt-5.4-mini"),
create_segments: createModel("openai/gpt-5.4-mini"),
youtube_login: createModel("openai/gpt-5.4-mini"),
web_deep_research: createModel("openai/gpt-5.4-mini"),
create_knowledge_base: createModel("openai/gpt-5.4-mini"),
send_email: createModel("openai/gpt-5.4-mini"),
};

// Map trigger tool -> sequence AFTER trigger
Expand Down
3 changes: 2 additions & 1 deletion lib/evals/scorers/CatalogAvailability.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DEFAULT_MODEL } from "@/lib/consts";
import { generateObject } from "ai";
import { createModel } from "@/lib/ai/createModel";
import { getCatalogDataAsCSV } from "@/lib/catalog/getCatalogDataAsCSV";
import { z } from "zod";

Expand All @@ -18,7 +19,7 @@ export const CatalogAvailability = async ({
const catalog = await getCatalogDataAsCSV(catalogId);

const result = await generateObject({
model: DEFAULT_MODEL,
model: createModel(DEFAULT_MODEL),
system: `You are a music catalog analyst. Your job is to analyze song recommendations and determine which ones are available in the provided music catalog.

Instructions:
Expand Down
3 changes: 2 additions & 1 deletion lib/evals/scorers/QuestionAnswered.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DEFAULT_MODEL } from "@/lib/consts";
import { generateObject } from "ai";
import { createModel } from "@/lib/ai/createModel";
import { z } from "zod";

/**
Expand All @@ -17,7 +18,7 @@ export const QuestionAnswered = async ({
}) => {
try {
const result = await generateObject({
model: DEFAULT_MODEL,
model: createModel(DEFAULT_MODEL),
system: `You are an AI evaluation expert. Your job is to determine if an AI assistant actually answered the customer's question with a specific answer, or if it deflected, explained why it couldn't answer, or gave generic suggestions without providing the requested information.

Instructions:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@composio/vercel": "^0.3.4",
"@fal-ai/client": "^1.9.5",
"@modelcontextprotocol/sdk": "^1.24.3",
"@openrouter/ai-sdk-provider": "^2.6.0",
"@privy-io/node": "^0.6.2",
"@supabase/supabase-js": "^2.86.0",
"@trigger.dev/sdk": "^4.4.3",
Expand Down
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

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