@matterailab/orbcode 0.1.13 → 0.1.14

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.
package/README.md CHANGED
@@ -157,6 +157,51 @@ The two Axon models are built in; `/model` opens a scroll-and-select picker
157
157
  image input. Cost comes from the API's usage chunks (`is_byok`-aware) and is
158
158
  shown in the status bar.
159
159
 
160
+ ### Other providers (Anthropic, OpenAI-compatible)
161
+
162
+ The Axon models go through the MatterAI gateway as before. A `customModels`
163
+ entry that sets a `provider` is instead served through the
164
+ [Vercel AI SDK](https://sdk.vercel.ai), reusing the same agent loop, tools, and
165
+ approvals — auth is the provider's own key (env var or `apiKey`), not the
166
+ MatterAI login.
167
+
168
+ ```json
169
+ {
170
+ "model": "claude-opus-4-8",
171
+ "customModels": [
172
+ {
173
+ "id": "claude-opus-4-8",
174
+ "name": "Claude Opus 4.8",
175
+ "provider": "anthropic",
176
+ "contextWindow": 1000000,
177
+ "maxOutputTokens": 64000,
178
+ "inputPrice": 0.000005,
179
+ "outputPrice": 0.000025,
180
+ "effort": "high"
181
+ },
182
+ {
183
+ "id": "some-model",
184
+ "provider": "openai-compatible",
185
+ "baseUrl": "https://api.other-host.com/v1"
186
+ }
187
+ ]
188
+ }
189
+ ```
190
+
191
+ - `provider: "anthropic"` → native `/v1/messages` (`@ai-sdk/anthropic`). Key from
192
+ `ANTHROPIC_API_KEY` (or `apiKey` on the entry). Adaptive thinking + reasoning
193
+ streaming are on by default; `effort` (`low`…`max`) tunes depth; prompt
194
+ caching breakpoints are set on the system prompt and conversation prefix
195
+ automatically. Thinking blocks are preserved across turns (stored with the
196
+ session and replayed with their signatures), so interleaved thinking with
197
+ tool use round-trips correctly. Set `"reasoning": false` to disable thinking
198
+ (e.g. for models that don't support `effort`).
199
+ - `provider: "openai-compatible"` → any OpenAI-compatible endpoint; requires
200
+ `baseUrl`. Key from `apiKey` on the entry.
201
+
202
+ Anything without a `provider` (or `provider: "matterai"`) keeps using the
203
+ MatterAI gateway untouched.
204
+
160
205
  ## The TUI
161
206
 
162
207
  ```
@@ -0,0 +1,260 @@
1
+ import { createAnthropic } from "@ai-sdk/anthropic";
2
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
3
+ import { jsonSchema, streamText, tool, } from "ai";
4
+ import { REASONING_DETAILS_FIELD } from "./llmClient.js";
5
+ /**
6
+ * LLM transport backed by the Vercel AI SDK. Serves any non-MatterAI provider
7
+ * (Anthropic native `/v1/messages`, or any OpenAI-compatible endpoint) while
8
+ * speaking the same OpenAI-shaped message/tool contract the agent loop uses —
9
+ * translation happens entirely at this boundary. Auth is the provider's own
10
+ * key (env var or settings), never the MatterAI token.
11
+ */
12
+ export class AiSdkClient {
13
+ options;
14
+ constructor(options) {
15
+ this.options = options;
16
+ }
17
+ async *createMessage(systemPrompt, messages, tools, abortSignal) {
18
+ const model = this.options.model;
19
+ const isAnthropic = model.provider === "anthropic";
20
+ // Replay stored thinking blocks only on Anthropic, where they round-trip
21
+ // with their signatures; other providers ignore the side-channel.
22
+ const aiMessages = toModelMessages(messages, isAnthropic);
23
+ if (isAnthropic)
24
+ applyCacheBreakpoint(aiMessages);
25
+ const aiTools = toAiTools(tools);
26
+ const hasTools = Object.keys(aiTools).length > 0;
27
+ const result = streamText({
28
+ model: this.resolveModel(),
29
+ system: this.buildSystem(systemPrompt, isAnthropic),
30
+ messages: aiMessages,
31
+ ...(hasTools ? { tools: aiTools, toolChoice: "auto" } : {}),
32
+ maxOutputTokens: model.maxOutputTokens,
33
+ // No `temperature`: the current Claude models reject sampling params.
34
+ abortSignal,
35
+ providerOptions: this.providerOptions(isAnthropic),
36
+ });
37
+ for await (const part of result.fullStream) {
38
+ switch (part.type) {
39
+ case "text-delta":
40
+ if (part.text)
41
+ yield { type: "text", text: part.text };
42
+ break;
43
+ case "reasoning-delta":
44
+ if (part.text)
45
+ yield { type: "reasoning", text: part.text };
46
+ break;
47
+ case "tool-call":
48
+ yield {
49
+ type: "native_tool_calls",
50
+ toolCalls: [
51
+ {
52
+ id: part.toolCallId,
53
+ type: "function",
54
+ function: {
55
+ name: part.toolName,
56
+ arguments: JSON.stringify(part.input ?? {}),
57
+ },
58
+ },
59
+ ],
60
+ };
61
+ break;
62
+ case "finish":
63
+ yield this.usageChunk(part.totalUsage);
64
+ break;
65
+ case "error":
66
+ throw asError(part.error);
67
+ }
68
+ }
69
+ // Capture this turn's thinking blocks (with their provider signatures) so
70
+ // the agent can stash them on the assistant message and replay them next
71
+ // turn — required for interleaved thinking + tool use on the same model.
72
+ const reasoning = collectReasoningParts((await result.response).messages);
73
+ if (reasoning.length > 0) {
74
+ yield { type: "reasoning_details", details: reasoning };
75
+ }
76
+ }
77
+ /** Build the provider model handle for this request. */
78
+ resolveModel() {
79
+ const { provider, id, baseUrl, apiKey } = this.options.model;
80
+ switch (provider) {
81
+ case "anthropic":
82
+ // createAnthropic reads ANTHROPIC_API_KEY from the env when apiKey is absent.
83
+ return createAnthropic(apiKey ? { apiKey } : {})(id);
84
+ default: {
85
+ // Any OpenAI-compatible endpoint (OpenRouter, Together, Groq, a local
86
+ // server, even api.openai.com/v1). Adding a native provider package
87
+ // (e.g. @ai-sdk/google) is a new `case` here.
88
+ if (!baseUrl) {
89
+ throw new Error(`Model "${id}" uses provider "${provider}" but no baseUrl is set. Add a "baseUrl" to its customModels entry in settings.json.`);
90
+ }
91
+ return createOpenAICompatible({ name: provider ?? "openai-compatible", baseURL: baseUrl, apiKey })(id);
92
+ }
93
+ }
94
+ }
95
+ /** Cache the system prompt (and, on Anthropic, the tool list with it). */
96
+ buildSystem(systemPrompt, isAnthropic) {
97
+ if (!isAnthropic)
98
+ return systemPrompt;
99
+ return {
100
+ role: "system",
101
+ content: systemPrompt,
102
+ providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
103
+ };
104
+ }
105
+ /** Adaptive thinking + effort + reasoning replay for Anthropic; nothing otherwise. */
106
+ providerOptions(isAnthropic) {
107
+ if (!isAnthropic)
108
+ return undefined;
109
+ const model = this.options.model;
110
+ if (model.reasoning === false)
111
+ return undefined;
112
+ return {
113
+ anthropic: {
114
+ thinking: { type: "adaptive", display: "summarized" },
115
+ effort: model.effort ?? "high",
116
+ sendReasoning: true,
117
+ },
118
+ };
119
+ }
120
+ usageChunk(usage) {
121
+ const model = this.options.model;
122
+ const inputTokens = usage.inputTokens ?? 0;
123
+ const outputTokens = usage.outputTokens ?? 0;
124
+ return {
125
+ type: "usage",
126
+ inputTokens,
127
+ outputTokens,
128
+ cacheReadTokens: usage.inputTokenDetails?.cacheReadTokens,
129
+ reasoningTokens: usage.outputTokenDetails?.reasoningTokens,
130
+ totalCost: model.free ? 0 : inputTokens * model.inputPrice + outputTokens * model.outputPrice,
131
+ inferenceProvider: model.provider,
132
+ };
133
+ }
134
+ }
135
+ /** Translate OpenAI-shaped function tools into the AI SDK's client-side tool set
136
+ * (no `execute` — the agent loop runs the tool and feeds the result back). */
137
+ function toAiTools(tools) {
138
+ const set = {};
139
+ for (const t of tools) {
140
+ if (t.type !== "function")
141
+ continue;
142
+ set[t.function.name] = tool({
143
+ description: t.function.description,
144
+ inputSchema: jsonSchema(t.function.parameters ?? { type: "object", properties: {} }),
145
+ });
146
+ }
147
+ return set;
148
+ }
149
+ /** Translate the OpenAI conversation history into AI SDK `ModelMessage`s.
150
+ * When `includeReasoning`, stored thinking blocks are prepended to each
151
+ * assistant turn (first, ahead of text/tool-call parts) for same-model replay. */
152
+ function toModelMessages(messages, includeReasoning) {
153
+ // tool_result parts need the tool name, which OpenAI only carries on the
154
+ // originating assistant tool_call — index it first.
155
+ const toolNameById = new Map();
156
+ for (const message of messages) {
157
+ if (message.role === "assistant" && message.tool_calls) {
158
+ for (const call of message.tool_calls) {
159
+ if (call.type === "function")
160
+ toolNameById.set(call.id, call.function.name);
161
+ }
162
+ }
163
+ }
164
+ const out = [];
165
+ for (const message of messages) {
166
+ switch (message.role) {
167
+ case "user":
168
+ out.push({ role: "user", content: contentToText(message.content) });
169
+ break;
170
+ case "assistant": {
171
+ const parts = [];
172
+ if (includeReasoning)
173
+ parts.push(...storedReasoningParts(message));
174
+ const text = contentToText(message.content);
175
+ if (text)
176
+ parts.push({ type: "text", text });
177
+ for (const call of message.tool_calls ?? []) {
178
+ if (call.type !== "function")
179
+ continue;
180
+ parts.push({
181
+ type: "tool-call",
182
+ toolCallId: call.id,
183
+ toolName: call.function.name,
184
+ input: safeParseJson(call.function.arguments),
185
+ });
186
+ }
187
+ out.push({ role: "assistant", content: parts.length > 0 ? parts : "" });
188
+ break;
189
+ }
190
+ case "tool": {
191
+ const result = {
192
+ type: "tool-result",
193
+ toolCallId: message.tool_call_id,
194
+ toolName: toolNameById.get(message.tool_call_id) ?? "tool",
195
+ output: { type: "text", value: contentToText(message.content) },
196
+ };
197
+ out.push({ role: "tool", content: [result] });
198
+ break;
199
+ }
200
+ // "system"/"developer"/"function" roles don't appear in OrbCode history
201
+ // (the system prompt is passed separately) — ignore defensively.
202
+ }
203
+ }
204
+ return out;
205
+ }
206
+ /** Pull the reasoning parts out of the assistant message(s) the SDK assembled
207
+ * for this turn — these carry the provider signature needed to replay them. */
208
+ function collectReasoningParts(messages) {
209
+ const parts = [];
210
+ for (const message of messages) {
211
+ if (message.role !== "assistant" || !Array.isArray(message.content))
212
+ continue;
213
+ for (const part of message.content) {
214
+ if (part.type === "reasoning")
215
+ parts.push(part);
216
+ }
217
+ }
218
+ return parts;
219
+ }
220
+ /** Read back the reasoning parts the agent stashed on a prior assistant turn. */
221
+ function storedReasoningParts(message) {
222
+ const details = message[REASONING_DETAILS_FIELD];
223
+ return Array.isArray(details) ? details : [];
224
+ }
225
+ /** Tag the last message so Anthropic caches the conversation prefix up to here. */
226
+ function applyCacheBreakpoint(messages) {
227
+ const last = messages[messages.length - 1];
228
+ if (!last)
229
+ return;
230
+ last.providerOptions = {
231
+ ...last.providerOptions,
232
+ anthropic: { ...last.providerOptions?.anthropic, cacheControl: { type: "ephemeral" } },
233
+ };
234
+ }
235
+ /** Flatten OpenAI string-or-parts content down to plain text. */
236
+ function contentToText(content) {
237
+ if (typeof content === "string")
238
+ return content;
239
+ if (Array.isArray(content)) {
240
+ return content
241
+ .map((part) => part && typeof part === "object" && "text" in part ? String(part.text ?? "") : "")
242
+ .join("");
243
+ }
244
+ return "";
245
+ }
246
+ function safeParseJson(raw) {
247
+ if (!raw)
248
+ return {};
249
+ try {
250
+ return JSON.parse(raw);
251
+ }
252
+ catch {
253
+ return {};
254
+ }
255
+ }
256
+ function asError(error) {
257
+ if (error instanceof Error)
258
+ return error;
259
+ return new Error(typeof error === "string" ? error : JSON.stringify(error));
260
+ }
@@ -1,6 +1,7 @@
1
1
  import OpenAI from "openai";
2
2
  import { API_GATEWAY_PATH, getUrlFromToken } from "../auth/auth.js";
3
3
  import { DEFAULT_HEADERS, X_AXONCODE_TASKID, X_AXON_REPO, X_ORGANIZATIONID } from "./headers.js";
4
+ import { stripReasoningDetails } from "./llmClient.js";
4
5
  import { getModel } from "./models.js";
5
6
  export class AxonClient {
6
7
  client;
@@ -26,7 +27,7 @@ export class AxonClient {
26
27
  const requestOptions = {
27
28
  model: model.id,
28
29
  temperature: 0.2,
29
- messages: [{ role: "system", content: systemPrompt }, ...messages],
30
+ messages: [{ role: "system", content: systemPrompt }, ...stripReasoningDetails(messages)],
30
31
  stream: true,
31
32
  stream_options: { include_usage: true },
32
33
  max_tokens: model.maxOutputTokens,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Non-standard field stashed on a persisted assistant message holding opaque
3
+ * reasoning blocks (with provider signatures) for same-model replay. Only the
4
+ * AI SDK client reads/writes it; it is stripped before any OpenAI request.
5
+ */
6
+ export const REASONING_DETAILS_FIELD = "_reasoningDetails";
7
+ /** Drop the reasoning side-channel from assistant messages bound for an OpenAI
8
+ * `/chat/completions` request (it isn't a valid input field there). */
9
+ export function stripReasoningDetails(messages) {
10
+ return messages.map((message) => {
11
+ if (message.role !== "assistant")
12
+ return message;
13
+ const record = message;
14
+ if (!(REASONING_DETAILS_FIELD in record))
15
+ return message;
16
+ const { [REASONING_DETAILS_FIELD]: _omit, ...rest } = record;
17
+ return rest;
18
+ });
19
+ }
@@ -1,4 +1,94 @@
1
+ /** Providers served by the built-in MatterAI gateway rather than the AI SDK. */
2
+ const MATTERAI_PROVIDERS = new Set(["matterai", "axon"]);
3
+ /** True when the model should be served via the Vercel AI SDK transport. */
4
+ export function usesAiSdk(model) {
5
+ return Boolean(model.provider) && !MATTERAI_PROVIDERS.has(model.provider);
6
+ }
7
+ /**
8
+ * Well-known Claude models, served natively via the Anthropic provider (AI SDK,
9
+ * `/v1/messages`). Built in so `--model claude-…` works without a settings.json
10
+ * entry; auth is `ANTHROPIC_API_KEY` (or a per-model `apiKey`), never MatterAI.
11
+ * Adaptive thinking + effort are on by default (see AiSdkClient); Haiku 4.5 sets
12
+ * `reasoning: false` because it rejects the `effort` parameter.
13
+ */
14
+ export const ANTHROPIC_MODELS = {
15
+ "claude-opus-4-8": {
16
+ id: "claude-opus-4-8",
17
+ name: "Claude Opus 4.8",
18
+ description: "Anthropic's most capable Opus model — long-horizon agentic work, knowledge work, and coding.",
19
+ contextWindow: 1_000_000,
20
+ maxOutputTokens: 64000,
21
+ supportsImages: true,
22
+ inputPrice: 0.000005,
23
+ outputPrice: 0.000025,
24
+ free: false,
25
+ provider: "anthropic",
26
+ },
27
+ "claude-opus-4-7": {
28
+ id: "claude-opus-4-7",
29
+ name: "Claude Opus 4.7",
30
+ description: "Previous-generation Opus — highly autonomous, strong on agentic, vision, and memory tasks.",
31
+ contextWindow: 1_000_000,
32
+ maxOutputTokens: 64000,
33
+ supportsImages: true,
34
+ inputPrice: 0.000005,
35
+ outputPrice: 0.000025,
36
+ free: false,
37
+ provider: "anthropic",
38
+ },
39
+ "claude-opus-4-6": {
40
+ id: "claude-opus-4-6",
41
+ name: "Claude Opus 4.6",
42
+ description: "Older Opus with adaptive thinking; 1M context.",
43
+ contextWindow: 1_000_000,
44
+ maxOutputTokens: 64000,
45
+ supportsImages: true,
46
+ inputPrice: 0.000005,
47
+ outputPrice: 0.000025,
48
+ free: false,
49
+ provider: "anthropic",
50
+ },
51
+ "claude-sonnet-4-6": {
52
+ id: "claude-sonnet-4-6",
53
+ name: "Claude Sonnet 4.6",
54
+ description: "Anthropic's best balance of speed and intelligence; adaptive thinking, 1M context.",
55
+ contextWindow: 1_000_000,
56
+ maxOutputTokens: 64000,
57
+ supportsImages: true,
58
+ inputPrice: 0.000003,
59
+ outputPrice: 0.000015,
60
+ free: false,
61
+ provider: "anthropic",
62
+ },
63
+ "claude-haiku-4-5": {
64
+ id: "claude-haiku-4-5",
65
+ name: "Claude Haiku 4.5",
66
+ description: "Fastest, most cost-effective Claude model for simple, latency-sensitive tasks.",
67
+ contextWindow: 200_000,
68
+ maxOutputTokens: 64000,
69
+ supportsImages: true,
70
+ inputPrice: 0.000001,
71
+ outputPrice: 0.000005,
72
+ free: false,
73
+ provider: "anthropic",
74
+ // Haiku 4.5 rejects the `effort` parameter, so don't send thinking/effort.
75
+ reasoning: false,
76
+ },
77
+ "claude-fable-5": {
78
+ id: "claude-fable-5",
79
+ name: "Claude Fable 5",
80
+ description: "Anthropic's most capable widely released model — most demanding reasoning and long-horizon work.",
81
+ contextWindow: 1_000_000,
82
+ maxOutputTokens: 64000,
83
+ supportsImages: true,
84
+ inputPrice: 0.00001,
85
+ outputPrice: 0.00005,
86
+ free: false,
87
+ provider: "anthropic",
88
+ },
89
+ };
1
90
  export const AXON_MODELS = {
91
+ ...ANTHROPIC_MODELS,
2
92
  "axon-code-2-5-mini": {
3
93
  id: "axon-code-2-5-mini",
4
94
  name: "Axon Code 2.5 Mini (free)",
@@ -32,6 +122,17 @@ export const AXON_MODELS = {
32
122
  outputPrice: 0.000009,
33
123
  free: false,
34
124
  },
125
+ "axon-eido-3-code-mini": {
126
+ id: "axon-eido-3-code-mini",
127
+ name: "Axon Eido 3 Mini",
128
+ description: "Axon Eido 3 Mini is a general purpose super intelligent LLM coding model for low-effort day-to-day tasks",
129
+ contextWindow: 400000,
130
+ maxOutputTokens: 64000,
131
+ supportsImages: true,
132
+ inputPrice: 0.000001,
133
+ outputPrice: 0.000003,
134
+ free: false,
135
+ },
35
136
  };
36
137
  export const DEFAULT_MODEL_ID = "axon-code-2-5-pro";
37
138
  /** Add user-defined models (from settings.json) to the registry. */
@@ -49,6 +150,11 @@ export function registerCustomModels(models) {
49
150
  inputPrice: model.inputPrice ?? 0,
50
151
  outputPrice: model.outputPrice ?? 0,
51
152
  free: (model.inputPrice ?? 0) === 0 && (model.outputPrice ?? 0) === 0,
153
+ provider: model.provider,
154
+ baseUrl: model.baseUrl,
155
+ apiKey: model.apiKey,
156
+ effort: model.effort,
157
+ reasoning: model.reasoning,
52
158
  };
53
159
  }
54
160
  }
@@ -0,0 +1,16 @@
1
+ import { AiSdkClient } from "./aiSdkClient.js";
2
+ import { AxonClient } from "./client.js";
3
+ import { getModel, usesAiSdk } from "./models.js";
4
+ /**
5
+ * Pick the transport for a model. MatterAI/Axon models keep using `AxonClient`
6
+ * (OpenAI `/chat/completions` against the gateway, with all its auth/headers/
7
+ * cost handling); anything declaring another `provider` goes through the Vercel
8
+ * AI SDK. Both implement `LLMClient`, so the agent loop is unaffected.
9
+ */
10
+ export function createLLMClient(options) {
11
+ const model = getModel(options.modelId);
12
+ if (usesAiSdk(model)) {
13
+ return new AiSdkClient({ model });
14
+ }
15
+ return new AxonClient(options);
16
+ }
@@ -1,7 +1,8 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import * as path from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
- import { AxonClient } from "../api/client.js";
4
+ import { REASONING_DETAILS_FIELD } from "../api/llmClient.js";
5
+ import { createLLMClient } from "../api/provider.js";
5
6
  import { buildSystemPrompt } from "../prompts/system.js";
6
7
  import { describeToolCall, executeTool, getActiveTools, getApprovalKind, } from "../tools/index.js";
7
8
  import { walkFiles } from "../tools/executors/listFiles.js";
@@ -107,7 +108,7 @@ export class Agent {
107
108
  }
108
109
  this.sessionApproveEdits = options.autoApproveEdits;
109
110
  this.systemPrompt = buildSystemPrompt(options.cwd);
110
- this.client = new AxonClient({
111
+ this.client = createLLMClient({
111
112
  token: options.token,
112
113
  modelId: options.modelId,
113
114
  taskId: this.taskId,
@@ -118,7 +119,7 @@ export class Agent {
118
119
  }
119
120
  setModel(modelId) {
120
121
  this.options.modelId = modelId;
121
- this.client = new AxonClient({
122
+ this.client = createLLMClient({
122
123
  token: this.options.token,
123
124
  modelId,
124
125
  taskId: this.taskId,
@@ -448,6 +449,7 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
448
449
  let assistantText = "";
449
450
  let hadReasoning = false;
450
451
  let reasoningStart = 0;
452
+ let reasoningDetails;
451
453
  const toolCallsByIndex = new Map();
452
454
  let nextSyntheticIndex = 10000;
453
455
  const stream = this.client.createMessage(this.systemPrompt, this.outgoingMessages(), getActiveTools(), signal);
@@ -466,6 +468,10 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
466
468
  }
467
469
  onEvent({ type: "reasoning-delta", text: chunk.text });
468
470
  break;
471
+ case "reasoning_details":
472
+ // Opaque thinking blocks (with signatures) for next-turn replay.
473
+ reasoningDetails = chunk.details;
474
+ break;
469
475
  case "native_tool_calls":
470
476
  for (const tc of chunk.toolCalls) {
471
477
  const index = tc.index ?? nextSyntheticIndex++;
@@ -513,6 +519,12 @@ User time zone: ${timeZone}, UTC${timeZoneOffsetStr}`;
513
519
  function: { name: tc.name, arguments: tc.arguments || "{}" },
514
520
  }));
515
521
  }
522
+ // Stash reasoning blocks (opaque) so the next turn can replay them. The
523
+ // field is persisted with the session and stripped on the OpenAI path.
524
+ if (reasoningDetails !== undefined) {
525
+ ;
526
+ assistantMessage[REASONING_DETAILS_FIELD] = reasoningDetails;
527
+ }
516
528
  this.messages.push(assistantMessage);
517
529
  if (toolCalls.length === 0) {
518
530
  return true;
package/dist/headless.js CHANGED
@@ -1,10 +1,23 @@
1
+ import { getModel, isValidAxonModel, usesAiSdk } from "./api/models.js";
1
2
  import { getAuthToken, getPendingProjectHooks, loadSettings } from "./config/settings.js";
2
3
  import { Agent } from "./core/agent.js";
3
4
  /** Non-interactive `orbcode -p "prompt"` mode: prints the final response to stdout. */
4
5
  export async function runHeadless(prompt, yolo) {
5
6
  const settings = loadSettings();
7
+ // An unknown --model (or ORBCODE_MODEL) silently resolves to the default; say
8
+ // so on stderr instead of quietly running a different model than requested.
9
+ const requestedModel = process.env.ORBCODE_MODEL;
10
+ if (requestedModel && !isValidAxonModel(requestedModel)) {
11
+ process.stderr.write(`warning: unknown model "${requestedModel}"; using "${settings.model}". ` +
12
+ `Add it under "customModels" in settings.json (with a "provider") to use it.\n`);
13
+ }
6
14
  const token = getAuthToken(settings);
7
- if (!token) {
15
+ // MatterAI/Axon models authenticate with the login token. AI-SDK providers
16
+ // (Anthropic, etc.) authenticate with their own key — resolved by the
17
+ // provider from the env (e.g. ANTHROPIC_API_KEY) or the model's `apiKey` —
18
+ // so they don't need a MatterAI login. Only gate on the token when the
19
+ // selected model actually goes through the MatterAI gateway.
20
+ if (!token && !usesAiSdk(getModel(settings.model))) {
8
21
  console.error("Not signed in. Run `orbcode login`, set ORBCODE_TOKEN, or put an apiKey in settings.json.");
9
22
  process.exit(1);
10
23
  }
@@ -24,7 +37,9 @@ export async function runHeadless(prompt, yolo) {
24
37
  let completionResult = "";
25
38
  const agent = new Agent({
26
39
  cwd: process.cwd(),
27
- token,
40
+ // May be empty for AI-SDK providers; AiSdkClient ignores it and uses the
41
+ // provider's own key. AxonClient only runs when a token is present.
42
+ token: token ?? "",
28
43
  modelId: settings.model,
29
44
  organizationId: settings.organizationId,
30
45
  baseUrl: settings.baseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matterailab/orbcode",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "OrbCode CLI — agentic coding in your terminal, powered by Axon models by MatterAI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,6 +36,9 @@
36
36
  "prepublishOnly": "npm run typecheck && npm run build"
37
37
  },
38
38
  "dependencies": {
39
+ "@ai-sdk/anthropic": "^3.0.85",
40
+ "@ai-sdk/openai-compatible": "^2.0.51",
41
+ "ai": "^6.0.207",
39
42
  "chalk": "^5.3.0",
40
43
  "ink": "^5.2.1",
41
44
  "open": "^10.1.0",