@meetopenbot/openbot 0.1.14 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ import { shouldUseCreditsAuth } from './credits-auth.js';
2
+ /** Product alias shown in the picker. Never sent to a provider or the credits proxy. */
3
+ export const AUTO_MODEL_ID = 'openbot/auto';
4
+ export const AUTO_MODEL_OPTION = {
5
+ value: AUTO_MODEL_ID,
6
+ label: 'Auto',
7
+ description: 'Fast and cheap. OpenBot picks the model.',
8
+ };
9
+ /**
10
+ * Cheap pool for Auto. Order is preference; failover walks this list.
11
+ * Intent routing can later pick a member without changing the alias.
12
+ */
13
+ export const AUTO_MODEL_POOL = [
14
+ 'openai/gpt-5.6-luna',
15
+ 'google/gemini-3.5-flash',
16
+ 'openai/gpt-5.4-nano',
17
+ ];
18
+ const PROVIDER_BYOK_ENV = {
19
+ openai: 'OPENAI_API_KEY',
20
+ anthropic: 'ANTHROPIC_API_KEY',
21
+ google: 'GOOGLE_GENERATIVE_AI_API_KEY',
22
+ deepseek: 'DEEPSEEK_API_KEY',
23
+ };
24
+ export function isAutoModel(modelString) {
25
+ if (!modelString)
26
+ return true;
27
+ const trimmed = modelString.trim();
28
+ return trimmed === '' || trimmed === AUTO_MODEL_ID || trimmed === 'auto';
29
+ }
30
+ function providerOf(modelString) {
31
+ return modelString.split('/')[0] ?? '';
32
+ }
33
+ function providerHasByokKey(provider, env) {
34
+ const key = PROVIDER_BYOK_ENV[provider];
35
+ return Boolean(key && env[key]?.trim());
36
+ }
37
+ /** Concrete models Auto may call, filtered by credits vs BYOK keys. */
38
+ export function listAutoModelCandidates(ctx) {
39
+ const pool = [...AUTO_MODEL_POOL];
40
+ if (shouldUseCreditsAuth(ctx))
41
+ return pool;
42
+ const env = ctx?.env ?? process.env;
43
+ const available = pool.filter((id) => providerHasByokKey(providerOf(id), env));
44
+ return available.length > 0 ? available : [pool[0]];
45
+ }
46
+ /** MVP picker: first available pool member. Later: classify, then pick. */
47
+ export function pickAutoModel(ctx) {
48
+ return listAutoModelCandidates(ctx)[0] ?? AUTO_MODEL_POOL[0];
49
+ }
50
+ /** Expand Auto to a concrete provider/id. Pass-through for pinned models. */
51
+ export function expandModelString(modelString, ctx) {
52
+ if (isAutoModel(modelString))
53
+ return pickAutoModel(ctx);
54
+ return modelString.trim();
55
+ }
56
+ export function isRetryableModelError(error) {
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ const lower = message.toLowerCase();
59
+ return (/\b(429|500|502|503|529)\b/.test(message) ||
60
+ lower.includes('not supported') ||
61
+ lower.includes('rate limit') ||
62
+ lower.includes('overloaded') ||
63
+ lower.includes('temporarily unavailable') ||
64
+ lower.includes('timeout') ||
65
+ lower.includes('econnreset') ||
66
+ lower.includes('fetch failed'));
67
+ }
package/dist/context.js CHANGED
@@ -1,5 +1,6 @@
1
- import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
2
- import { todoService } from './tools/todo-service.js';
1
+ import { GENERAL_SPACE_ID } from "./space-id.js";
2
+ import { OPENBOT_SYSTEM_PROMPT } from "./system-prompt.js";
3
+ import { todoService } from "./tools/todo-service.js";
3
4
  export const DEFAULT_CONTEXT_BUDGET = 8000;
4
5
  export const MAX_CONTEXT_FILES = 50;
5
6
  /**
@@ -7,17 +8,17 @@ export const MAX_CONTEXT_FILES = 50;
7
8
  */
8
9
  export const getContextBudgetForModel = (modelString) => {
9
10
  const budgets = {
10
- 'openai/gpt-4o': 128000,
11
- 'openai/gpt-4o-mini': 128000,
12
- 'openai/o1-preview': 128000,
13
- 'openai/o1-mini': 128000,
14
- 'anthropic/claude-3-5-sonnet-20240620': 200000,
15
- 'anthropic/claude-3-5-sonnet-latest': 200000,
16
- 'anthropic/claude-3-opus-20240229': 200000,
17
- 'anthropic/claude-3-sonnet-20240229': 200000,
18
- 'anthropic/claude-3-haiku-20240307': 200000,
19
- 'deepseek/deepseek-chat': 64000,
20
- 'deepseek/deepseek-reasoner': 64000,
11
+ "openai/gpt-4o": 128000,
12
+ "openai/gpt-4o-mini": 128000,
13
+ "openai/o1-preview": 128000,
14
+ "openai/o1-mini": 128000,
15
+ "anthropic/claude-3-5-sonnet-20240620": 200000,
16
+ "anthropic/claude-3-5-sonnet-latest": 200000,
17
+ "anthropic/claude-3-opus-20240229": 200000,
18
+ "anthropic/claude-3-sonnet-20240229": 200000,
19
+ "anthropic/claude-3-haiku-20240307": 200000,
20
+ "deepseek/deepseek-chat": 64000,
21
+ "deepseek/deepseek-reasoner": 64000,
21
22
  };
22
23
  return budgets[modelString] || DEFAULT_CONTEXT_BUDGET;
23
24
  };
@@ -25,18 +26,24 @@ export const getContextBudgetForModel = (modelString) => {
25
26
  * Simplified context builder for MVP.
26
27
  */
27
28
  export async function buildContext(state, storage) {
28
- const { channelId, threadId, channelDetails, agentId, threadDetails, agentDetails } = state;
29
+ const { channelId, threadId, channelDetails, agentId, threadDetails, agentDetails, } = state;
29
30
  const sections = [];
30
31
  // Fetch agents once if storage is available
31
- const allAgents = storage?.getAgents ? await storage.getAgents().catch(() => []) : [];
32
+ const allAgents = storage?.getAgents
33
+ ? await storage.getAgents().catch(() => [])
34
+ : [];
32
35
  // 1. User
33
36
  if (state.currentUser?.userName) {
34
37
  sections.push(`## HUMAN\n- Name: ${state.currentUser.userName}`);
35
38
  }
36
39
  // 2. Environment
37
- let env = '## ENVIRONMENT\n';
38
- const channelName = channelDetails?.name || channelId;
39
- env += `- Mode: Channel (#${channelName})\n`;
40
+ let env = "## ENVIRONMENT\n";
41
+ const spaceName = channelDetails?.name || channelId;
42
+ const isGeneral = channelId === GENERAL_SPACE_ID;
43
+ env += `- Space: #${channelId || "unknown"} (${spaceName || channelId})\n`;
44
+ env += isGeneral
45
+ ? "- Role: inbox — answer questions, listings, and setup here. start_work only for real project work into an existing Space; propose new Spaces before creating them\n"
46
+ : "- Role: dedicated Space — do the work here; do not start_work unless the human asks to move\n";
40
47
  if (channelDetails?.cwd) {
41
48
  env += `- Workspace: ${channelDetails.cwd}\n`;
42
49
  }
@@ -44,6 +51,45 @@ export async function buildContext(state, storage) {
44
51
  env += `- Thread: ${threadDetails?.name || threadId}\n`;
45
52
  }
46
53
  sections.push(env);
54
+ if (threadId) {
55
+ const threadState = threadDetails?.state && typeof threadDetails.state === "object"
56
+ ? threadDetails.state
57
+ : undefined;
58
+ const threadStatus = typeof threadState?.status === "string" && threadState.status.trim()
59
+ ? threadState.status.trim()
60
+ : "completed";
61
+ const threadStatusReason = typeof threadState?.statusReason === "string" && threadState.statusReason.trim()
62
+ ? threadState.statusReason.trim()
63
+ : undefined;
64
+ let statusSection = `## THREAD STATUS\n- Status: ${threadStatus}`;
65
+ if (threadStatusReason) {
66
+ statusSection += `\n- Reason: ${threadStatusReason}`;
67
+ }
68
+ statusSection +=
69
+ "\nUse `set_thread_status` to update this. needs_input when blocked on the human; ready_for_review when you believe the job is done. Never set completed or archived.";
70
+ sections.push(statusSection);
71
+ }
72
+ const invokeData = state.triggerEvent?.type === "agent:invoke"
73
+ ? state.triggerEvent.data
74
+ : undefined;
75
+ const mentionedAgentIds = Array.isArray(invokeData?.mentionedAgentIds)
76
+ ? invokeData.mentionedAgentIds.filter((id) => typeof id === "string" && id.length > 0)
77
+ : [];
78
+ const forwardedFromSpaceId = typeof invokeData?.forwardedFromSpaceId === "string"
79
+ ? invokeData.forwardedFromSpaceId
80
+ : undefined;
81
+ if (mentionedAgentIds.length > 0) {
82
+ const formatted = mentionedAgentIds
83
+ .map((id) => {
84
+ const agent = allAgents.find((a) => a.id === id);
85
+ return `- ${id}${agent?.name && agent.name !== id ? ` (${agent.name})` : ""} — you MUST ask this agent`;
86
+ })
87
+ .join("\n");
88
+ sections.push(`## MENTIONED AGENTS\n${formatted}`);
89
+ }
90
+ if (forwardedFromSpaceId) {
91
+ sections.push(`## FORWARDED WORK\nThis thread was started from #${forwardedFromSpaceId}. Stay in this Space. Do not call start_work again.`);
92
+ }
47
93
  // 2.5 Thread todos
48
94
  if (channelId && threadId) {
49
95
  try {
@@ -51,35 +97,72 @@ export async function buildContext(state, storage) {
51
97
  if (list.items.length > 0) {
52
98
  const formatted = list.items
53
99
  .map((t) => `- [${t.status}] (${t.id}) ${t.content}`)
54
- .join('\n');
100
+ .join("\n");
55
101
  sections.push(`## TODOS\n${formatted}`);
56
102
  }
57
103
  }
58
104
  catch (error) {
59
- console.warn('[context] Failed to fetch todos:', error);
105
+ console.warn("[context] Failed to fetch todos:", error);
60
106
  }
61
107
  }
62
108
  // 2.6 Installed Agents
63
109
  if (allAgents.length > 0) {
64
110
  const formatted = allAgents
65
- .map((a) => `- ${a.id}: ${a.name}${a.description ? ` - ${a.description}` : ''}`)
66
- .join('\n');
67
- sections.push(`## INSTALLED AGENTS\n${formatted}`);
111
+ .map((a) => `- ${a.id}: ${a.name}${a.description ? ` - ${a.description}` : ""}`)
112
+ .join("\n");
113
+ sections.push(`## INSTALLED AGENTS\n${formatted}\n\nAnswer questions about people from this list. Do not create a Space to inspect agents.`);
114
+ }
115
+ if (storage?.getChannels) {
116
+ try {
117
+ const spaces = (await storage.getChannels());
118
+ if (spaces.length > 0) {
119
+ const formatted = (await Promise.all(spaces.map(async (space) => {
120
+ let specHint = "";
121
+ if (storage.getChannelDetails) {
122
+ try {
123
+ const details = await storage.getChannelDetails({
124
+ channelId: space.id,
125
+ });
126
+ const spec = typeof details?.spec === "string"
127
+ ? details.spec.trim()
128
+ : "";
129
+ const excerpt = spec
130
+ .split("\n")
131
+ .map((line) => line.trim())
132
+ .filter((line) => line && !line.startsWith("#"))
133
+ .join(" ")
134
+ .slice(0, 140);
135
+ if (excerpt)
136
+ specHint = ` — ${excerpt}`;
137
+ }
138
+ catch {
139
+ // ignore spec lookup failures
140
+ }
141
+ }
142
+ const inbox = space.id === GENERAL_SPACE_ID ? " [inbox]" : "";
143
+ return `- ${space.id}: ${space.name || space.id}${inbox}${specHint}`;
144
+ }))).join("\n");
145
+ sections.push(`## SPACES\n${formatted}\n\nAnswer questions about available Spaces from this list. Do not create a Space to look them up.`);
146
+ }
147
+ }
148
+ catch (error) {
149
+ console.warn("[context] Failed to fetch spaces:", error);
150
+ }
68
151
  }
69
- // 3. Channel Spec
152
+ // 3. Space spec
70
153
  const spec = channelDetails?.spec?.trim();
71
154
  if (spec) {
72
- sections.push(`## CHANNEL SPECIFICATION\n${spec}`);
155
+ sections.push(`## SPACE SPEC\n${spec}`);
73
156
  }
74
- // 4. Files
75
- if (storage?.listFiles && channelId && channelDetails?.cwd) {
157
+ // 4. Files (dedicated Spaces only — general is an inbox)
158
+ if (!isGeneral && storage?.listFiles && channelId && channelDetails?.cwd) {
76
159
  try {
77
160
  const files = await storage.listFiles({ channelId });
78
161
  if (files.length > 0) {
79
162
  const limited = files.slice(0, MAX_CONTEXT_FILES);
80
163
  const formatted = limited
81
- .map((f) => `- ${f.name}${f.isDirectory ? '/' : ''}`)
82
- .join('\n');
164
+ .map((f) => `- ${f.name}${f.isDirectory ? "/" : ""}`)
165
+ .join("\n");
83
166
  let fileSection = `## FILES\n${formatted}`;
84
167
  if (files.length > MAX_CONTEXT_FILES) {
85
168
  fileSection += `\n- ... and ${files.length - MAX_CONTEXT_FILES} more files`;
@@ -87,36 +170,35 @@ export async function buildContext(state, storage) {
87
170
  sections.push(fileSection);
88
171
  }
89
172
  else {
90
- sections.push('## FILES\n- (No files in workspace)');
173
+ sections.push("## FILES\n- (No files in workspace)");
91
174
  }
92
175
  }
93
176
  catch (error) {
94
- console.warn('[context] Failed to fetch files:', error);
177
+ console.warn("[context] Failed to fetch files:", error);
95
178
  }
96
179
  }
97
180
  // 5. Agent Instructions
98
181
  const rawInstructions = agentDetails?.instructions?.trim();
99
- if (rawInstructions &&
100
- rawInstructions !== OPENBOT_SYSTEM_PROMPT.trim()) {
182
+ if (rawInstructions && rawInstructions !== OPENBOT_SYSTEM_PROMPT.trim()) {
101
183
  sections.push(`## Instructions\n${rawInstructions}`);
102
184
  }
103
185
  // 6. Memories
104
186
  if (storage?.listMemories) {
105
187
  try {
106
- const scopes = ['global', `agent:${agentId}`];
188
+ const scopes = ["global", `agent:${agentId}`];
107
189
  if (channelId)
108
190
  scopes.push(`channel:${channelId}`);
109
191
  const records = await storage.listMemories({ scopes, limit: 20 });
110
192
  if (records.length > 0) {
111
193
  const formatted = records
112
194
  .map((r) => `- (${r.scope}) ${r.content}`)
113
- .join('\n');
195
+ .join("\n");
114
196
  sections.push(`## MEMORIES\n${formatted}`);
115
197
  }
116
198
  }
117
199
  catch (error) {
118
- console.warn('[context] Failed to fetch memories:', error);
200
+ console.warn("[context] Failed to fetch memories:", error);
119
201
  }
120
202
  }
121
- return sections.join('\n\n');
203
+ return sections.join("\n\n");
122
204
  }
package/dist/history.js CHANGED
@@ -89,8 +89,9 @@ export function eventsToModelMessages(events) {
89
89
  case 'agent:invoke': {
90
90
  const invokeEvent = event;
91
91
  if (invokeEvent.data?.content && invokeEvent.data?.role) {
92
+ const role = invokeEvent.data.role === 'agent' ? 'user' : invokeEvent.data.role;
92
93
  messages.push({
93
- role: invokeEvent.data.role,
94
+ role,
94
95
  content: invokeEvent.data.content,
95
96
  });
96
97
  }
package/dist/index.js CHANGED
@@ -1,37 +1,45 @@
1
- import { defineOpenbotPlugin } from './types.js';
2
- import { isCloudMode } from './cloud-mode.js';
3
- import { resolveModelConfigField } from './model-registry.js';
4
- import { openbotRuntime } from './runtime.js';
5
- import { bashPlugin } from './tools/bash.js';
6
- import { memoryPlugin } from './tools/memory.js';
7
- import { todoPlugin } from './tools/todo.js';
8
- import { approvalPlugin } from './tools/approval.js';
9
- import { delegationPlugin } from './tools/delegation.js';
10
- import { uiPlugin } from './tools/ui.js';
11
- import { previewPlugin } from './tools/preview.js';
12
- import { storageToolPlugin } from './tools/storage.js';
13
- export const OPENBOT_PLUGIN_ID = '@meetopenbot/openbot';
1
+ import { defineOpenbotPlugin } from "./types.js";
2
+ import { isCloudMode } from "./cloud-mode.js";
3
+ import { AUTO_MODEL_ID } from "./auto-model.js";
4
+ import { resolveModelConfigField } from "./model-registry.js";
5
+ import { openbotRuntime } from "./runtime.js";
6
+ import { bashPlugin } from "./tools/bash.js";
7
+ import { memoryPlugin } from "./tools/memory.js";
8
+ import { todoPlugin } from "./tools/todo.js";
9
+ import { approvalPlugin } from "./tools/approval.js";
10
+ import { askAgentPlugin } from "./tools/ask-agent.js";
11
+ import { startWorkPlugin } from "./tools/start-work.js";
12
+ import { threadStatusPlugin } from "./tools/thread-status.js";
13
+ import { uiPlugin } from "./tools/ui.js";
14
+ import { previewPlugin } from "./tools/preview.js";
15
+ import { storageToolPlugin } from "./tools/storage.js";
16
+ export const OPENBOT_PLUGIN_ID = "@meetopenbot/openbot";
14
17
  const modelField = await resolveModelConfigField();
18
+ const SPECIALIST_TOOL_NAMES = new Set([
19
+ ...Object.keys(bashPlugin.toolDefinitions ?? {}),
20
+ ...Object.keys(previewPlugin.toolDefinitions ?? {}),
21
+ ]);
15
22
  /**
16
23
  * `@meetopenbot/openbot` — the standard, opinionated OpenBot agent runtime.
17
24
  *
18
- * Batteries-included: shell, memory, todo, storage tools, delegation, approval,
19
- * preview, and the LLM loop.
25
+ * The orchestrator (`system`) is a coordinator: ask agents, start work in
26
+ * Spaces, memory, todos, storage. Specialist tools (shell, preview) stay on
27
+ * non-orchestrator agents that opt into this runtime.
20
28
  */
21
29
  export const openbotPlugin = defineOpenbotPlugin({
22
30
  id: OPENBOT_PLUGIN_ID,
23
- name: 'OpenBot Agent',
24
- description: 'The standard OpenBot agent runtime with inbuilt tools (shell, memory, todo, storage, delegation, and approval).',
31
+ name: "OpenBot Agent",
32
+ description: "OpenBot coordinator runtime: ask specialists, route work into Spaces, and track todos.",
25
33
  configSchema: {
26
- type: 'object',
34
+ type: "object",
27
35
  properties: {
28
36
  ...(isCloudMode()
29
37
  ? {
30
38
  authMode: {
31
- type: 'string',
32
- description: 'Credits — use your workspace credit balance via OpenBot. BYOK — bring your own API key.',
33
- enum: ['credits', 'byok'],
34
- default: 'credits',
39
+ type: "string",
40
+ description: "Credits — use your workspace credit balance via OpenBot. BYOK — bring your own API key.",
41
+ enum: ["credits", "byok"],
42
+ default: "credits",
35
43
  },
36
44
  }
37
45
  : {}),
@@ -43,31 +51,41 @@ export const openbotPlugin = defineOpenbotPlugin({
43
51
  ...memoryPlugin.toolDefinitions,
44
52
  ...todoPlugin.toolDefinitions,
45
53
  ...storageToolPlugin.toolDefinitions,
46
- ...delegationPlugin.toolDefinitions,
54
+ ...askAgentPlugin.toolDefinitions,
55
+ ...startWorkPlugin.toolDefinitions,
56
+ ...threadStatusPlugin.toolDefinitions,
47
57
  ...previewPlugin.toolDefinitions,
48
58
  },
49
59
  factory: (context) => (builder) => {
50
60
  const { agentId, config, storage, tools, abortSignal, host } = context;
51
- bashPlugin.factory(context)(builder);
61
+ const isOrchestrator = agentId === host.orchestratorAgentId;
52
62
  memoryPlugin.factory(context)(builder);
53
63
  todoPlugin.factory(context)(builder);
54
64
  storageToolPlugin.register(context)(builder);
55
- delegationPlugin.factory(context)(builder);
65
+ askAgentPlugin.factory(context)(builder);
66
+ startWorkPlugin.factory(context)(builder);
67
+ threadStatusPlugin.factory(context)(builder);
56
68
  uiPlugin.factory(context)(builder);
57
- previewPlugin.factory(context)(builder);
69
+ if (!isOrchestrator) {
70
+ bashPlugin.factory(context)(builder);
71
+ previewPlugin.factory(context)(builder);
72
+ }
58
73
  const approvalConfig = config?.approval ?? {
59
74
  actions: [],
60
75
  };
61
76
  approvalPlugin.factory({ ...context, config: approvalConfig })(builder);
62
77
  const authMode = host.isCloudSystemAgent(agentId)
63
78
  ? host.parseOpenbotAuthMode(config?.authMode)
64
- : 'byok';
79
+ : "byok";
80
+ const toolDefinitions = isOrchestrator
81
+ ? Object.fromEntries(Object.entries(tools).filter(([name]) => !SPECIALIST_TOOL_NAMES.has(name)))
82
+ : tools;
65
83
  return openbotRuntime({
66
- model: config?.model,
84
+ model: config?.model || AUTO_MODEL_ID,
67
85
  authMode,
68
86
  agentId,
69
87
  storage,
70
- toolDefinitions: tools,
88
+ toolDefinitions,
71
89
  abortSignal,
72
90
  host,
73
91
  })(builder);
@@ -1,3 +1,4 @@
1
+ import { AUTO_MODEL_ID, AUTO_MODEL_OPTION } from './auto-model.js';
1
2
  const DEFAULT_REGISTRY_URL = 'https://raw.githubusercontent.com/meetopenbot/openbot-registry/main/registry.json';
2
3
  const API_KEY_PROVIDER_IDS = ['openai', 'anthropic', 'google', 'deepseek'];
3
4
  export const PROVIDER_API_KEY_LINKS = {
@@ -67,26 +68,26 @@ function listAllModelOptions(registry) {
67
68
  }
68
69
  return options;
69
70
  }
71
+ function withAutoOption(options) {
72
+ if (options.some((option) => option.value === AUTO_MODEL_ID))
73
+ return options;
74
+ return [AUTO_MODEL_OPTION, ...options];
75
+ }
70
76
  const freeInputModelField = () => ({
71
77
  type: 'string',
72
- override: true,
73
- description: 'Provider model in provider/model-id format (e.g. openai/gpt-4o-mini).',
74
- default: 'openai/gpt-4o-mini',
78
+ description: 'Auto, or a provider model in provider/model-id format (e.g. openai/gpt-4o-mini).',
79
+ default: AUTO_MODEL_ID,
75
80
  });
76
81
  /** Registry-backed model field for plugin configSchema (`enum` + labeled `options`). */
77
82
  export async function resolveModelConfigField() {
78
83
  const registry = await fetchModelRegistry();
79
- const options = listAllModelOptions(registry);
80
- if (options.length === 0)
84
+ const options = withAutoOption(listAllModelOptions(registry));
85
+ if (options.length === 1)
81
86
  return freeInputModelField();
82
- const defaultModel = options.find((option) => option.value === 'openai/gpt-4o-mini')?.value ??
83
- options.find((option) => option.value.startsWith('openai/'))?.value ??
84
- options[0].value;
85
87
  return {
86
88
  type: 'string',
87
- override: true,
88
- description: 'Model from the OpenBot registry.',
89
- default: defaultModel,
89
+ description: 'Auto, or a model from the OpenBot registry.',
90
+ default: AUTO_MODEL_ID,
90
91
  enum: options.map((option) => option.value),
91
92
  options,
92
93
  };
package/dist/model.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createOpenAI, openai as defaultOpenai } from '@ai-sdk/openai';
2
2
  import { createAnthropic, anthropic } from '@ai-sdk/anthropic';
3
+ import { expandModelString } from './auto-model.js';
3
4
  import { CREDITS_API_KEY_PLACEHOLDER, INTEGRATIONS_TOKEN_HEADER, creditsProviderBaseUrl, resolveCreditsAuthConfig, shouldUseCreditsAuth, } from './credits-auth.js';
4
5
  const defaultDeepseek = createOpenAI({
5
6
  baseURL: 'https://api.deepseek.com',
@@ -9,10 +10,18 @@ const defaultGoogle = createOpenAI({
9
10
  baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai',
10
11
  apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
11
12
  });
12
- /** AI SDK 5+ defaults `openai(modelId)` to the Responses API; Gemini compat needs chat. */
13
+ /**
14
+ * OpenAI-compatible Chat Completions. Gemini/DeepSeek gateways only speak this.
15
+ * Do not use for OpenAI itself — gpt-5.6-luna rejects function tools on
16
+ * `/v1/chat/completions` unless `reasoning_effort` is `none`.
17
+ */
13
18
  function openAiChatModel(options, modelId) {
14
19
  return createOpenAI(options).chat(modelId);
15
20
  }
21
+ /** AI SDK 5+ default: OpenAI Responses API (`/v1/responses`), required for tools on Luna. */
22
+ function openAiResponsesModel(options, modelId) {
23
+ return createOpenAI(options)(modelId);
24
+ }
16
25
  function resolveCreditsProvider(provider, modelId) {
17
26
  const config = resolveCreditsAuthConfig();
18
27
  if (!config) {
@@ -24,7 +33,7 @@ function resolveCreditsProvider(provider, modelId) {
24
33
  const apiKey = config.token || CREDITS_API_KEY_PLACEHOLDER;
25
34
  switch (provider) {
26
35
  case 'openai':
27
- return openAiChatModel({ baseURL, apiKey, headers }, modelId);
36
+ return openAiResponsesModel({ baseURL, apiKey, headers }, modelId);
28
37
  case 'anthropic':
29
38
  return createAnthropic({ baseURL, apiKey, headers })(modelId);
30
39
  case 'google':
@@ -34,7 +43,8 @@ function resolveCreditsProvider(provider, modelId) {
34
43
  }
35
44
  }
36
45
  export function resolveModel(modelString, options) {
37
- const [provider, ...rest] = modelString.split('/');
46
+ const concrete = expandModelString(modelString, options);
47
+ const [provider, ...rest] = concrete.split('/');
38
48
  const modelId = rest.join('/');
39
49
  if (!modelId) {
40
50
  throw new Error(`Invalid model string: "${modelString}". Expected "provider/model-id".`);
@@ -42,7 +52,7 @@ export function resolveModel(modelString, options) {
42
52
  const useCredits = shouldUseCreditsAuth(options);
43
53
  switch (provider) {
44
54
  case 'openai':
45
- return useCredits ? resolveCreditsProvider('openai', modelId) : defaultOpenai.chat(modelId);
55
+ return useCredits ? resolveCreditsProvider('openai', modelId) : defaultOpenai(modelId);
46
56
  case 'anthropic':
47
57
  return useCredits ? resolveCreditsProvider('anthropic', modelId) : anthropic(modelId);
48
58
  case 'google':