axexec 1.5.1 → 1.6.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.
package/README.md CHANGED
@@ -88,6 +88,7 @@ axexec --list-agents | tail -n +2 | awk -F'\t' '$3 ~ /openai/ {print $1}'
88
88
  -a, --agent <id> Agent to use (claude, codex, gemini, opencode, copilot)
89
89
  -p, --prompt <text> Prompt text (alternative to positional argument)
90
90
  -m, --model <model> Model to use (agent-specific)
91
+ --provider <provider> Provider for OpenCode (anthropic, openai, google)
91
92
  --allow <perms> Permission rules to allow (comma-separated)
92
93
  --deny <perms> Permission rules to deny (comma-separated)
93
94
  -f, --format <fmt> Output format: jsonl, tsv (default: tsv, truncated on TTY)
@@ -165,19 +166,21 @@ For Claude, `CLAUDE_CODE_OAUTH_TOKEN` (generated via `claude setup-token`) also
165
166
  For Copilot, token precedence is `COPILOT_GITHUB_TOKEN`, then `GH_TOKEN`, then
166
167
  `GITHUB_TOKEN`.
167
168
 
168
- For OpenCode, if multiple provider API keys are set, axexec uses the model
169
- prefix (`anthropic/...`, `openai/...`, `google/...`) to disambiguate. When
170
- multiple keys are present and no provider prefix is available, set
171
- `AX_OPENCODE_CREDENTIALS` explicitly. This value must be a JSON-encoded
172
- Credentials object (not a raw API key), for example:
173
-
174
- ```json
175
- {
176
- "agent": "opencode",
177
- "type": "api-key",
178
- "provider": "openai",
179
- "data": { "apiKey": "sk-..." }
180
- }
169
+ For OpenCode, set `AX_OPENCODE_CREDENTIALS` with your credentials. The `provider`
170
+ field must match your `--provider` flag:
171
+
172
+ ```bash
173
+ # Anthropic (provider defaults to "anthropic" if omitted)
174
+ AX_OPENCODE_CREDENTIALS='{"agent":"opencode","type":"api-key","data":{"apiKey":"sk-ant-..."}}' \
175
+ axexec -a opencode --provider anthropic -m claude-sonnet-4 "Hello"
176
+
177
+ # OpenAI (provider must be specified in credentials)
178
+ AX_OPENCODE_CREDENTIALS='{"agent":"opencode","type":"api-key","provider":"openai","data":{"apiKey":"sk-..."}}' \
179
+ axexec -a opencode --provider openai -m gpt-4.1 "Hello"
180
+
181
+ # OpenCode hosted models (provider must be "opencode")
182
+ AX_OPENCODE_CREDENTIALS='{"agent":"opencode","type":"api-key","provider":"opencode","data":{"apiKey":"..."}}' \
183
+ axexec -a opencode --provider opencode -m glm-4.7-free "Hello"
181
184
  ```
182
185
 
183
186
  ## Isolation
@@ -73,12 +73,13 @@ async function* streamSession(options) {
73
73
  const session = await createSession(serverUrl, cwd);
74
74
  sessionId = session.id;
75
75
  sessionStartTime = Date.now();
76
- await sendPrompt(serverUrl, sessionId, options.prompt, cwd, options.model);
76
+ await sendPrompt(serverUrl, sessionId, options.prompt, cwd, options.provider, options.model);
77
77
  // 5. Process SSE events
78
78
  const eventProcessor = processSSEEvents(sseGenerator, {
79
79
  serverUrl,
80
80
  sessionId,
81
81
  sessionStartTime,
82
+ requestedProvider: options.provider,
82
83
  requestedModel: options.model,
83
84
  cwd,
84
85
  signal,
@@ -103,6 +104,7 @@ async function* streamSession(options) {
103
104
  sessionId,
104
105
  timestamp: sessionStartTime ?? Date.now(),
105
106
  modelInfo: undefined,
107
+ requestedProvider: options.provider,
106
108
  requestedModel: options.model,
107
109
  });
108
110
  }
@@ -9,6 +9,7 @@ interface SessionStartEventOptions {
9
9
  sessionId: string;
10
10
  timestamp: number;
11
11
  modelInfo: MessageModelInfo | undefined;
12
+ requestedProvider: string | undefined;
12
13
  requestedModel: string | undefined;
13
14
  }
14
15
  /**
@@ -7,11 +7,9 @@
7
7
  * Creates a session.start event with model and provider info.
8
8
  */
9
9
  function createSessionStartEvent(options) {
10
- const { sessionId, timestamp, modelInfo, requestedModel } = options;
11
- const model = modelInfo
12
- ? `${modelInfo.providerID}/${modelInfo.modelID}`
13
- : (requestedModel ?? "unknown");
14
- const provider = modelInfo?.providerID;
10
+ const { sessionId, timestamp, modelInfo, requestedProvider, requestedModel } = options;
11
+ const model = modelInfo?.modelID ?? requestedModel ?? "unknown";
12
+ const provider = modelInfo?.providerID ?? requestedProvider;
15
13
  return {
16
14
  type: "session.start",
17
15
  sessionId,
@@ -7,6 +7,7 @@ interface ProcessEventsContext {
7
7
  serverUrl: string;
8
8
  sessionId: string;
9
9
  sessionStartTime: number;
10
+ requestedProvider?: string;
10
11
  requestedModel?: string;
11
12
  cwd: string;
12
13
  signal: AbortSignal;
@@ -27,6 +27,7 @@ async function* processSSEEvents(sseGenerator, context) {
27
27
  sessionId: context.sessionId,
28
28
  timestamp: context.sessionStartTime,
29
29
  modelInfo,
30
+ requestedProvider: context.requestedProvider,
30
31
  requestedModel: context.requestedModel,
31
32
  });
32
33
  sessionStartEmitted = true;
@@ -38,6 +39,7 @@ async function* processSSEEvents(sseGenerator, context) {
38
39
  sessionId: context.sessionId,
39
40
  timestamp: context.sessionStartTime,
40
41
  modelInfo: undefined,
42
+ requestedProvider: context.requestedProvider,
41
43
  requestedModel: context.requestedModel,
42
44
  });
43
45
  sessionStartEmitted = true;
@@ -37,7 +37,7 @@ declare function createSession(baseUrl: string, directory: string): Promise<Sess
37
37
  * Uses prompt_async endpoint which returns immediately and processes
38
38
  * the prompt asynchronously. Events are streamed via SSE.
39
39
  */
40
- declare function sendPrompt(baseUrl: string, sessionId: string, prompt: string, directory: string, model?: string): Promise<void>;
40
+ declare function sendPrompt(baseUrl: string, sessionId: string, prompt: string, directory: string, provider?: string, model?: string): Promise<void>;
41
41
  /**
42
42
  * Aborts a running session with timeout.
43
43
  */
@@ -21,16 +21,6 @@ class PromptSubmitError extends Error {
21
21
  this.statusCode = statusCode;
22
22
  }
23
23
  }
24
- /** Parses "provider/model" string into components */
25
- function parseModel(model) {
26
- const slashIndex = model.indexOf("/");
27
- if (slashIndex === -1)
28
- return undefined;
29
- return {
30
- providerID: model.slice(0, slashIndex),
31
- modelID: model.slice(slashIndex + 1),
32
- };
33
- }
34
24
  /**
35
25
  * Creates a new session on the OpenCode server.
36
26
  */
@@ -54,17 +44,14 @@ async function createSession(baseUrl, directory) {
54
44
  * Uses prompt_async endpoint which returns immediately and processes
55
45
  * the prompt asynchronously. Events are streamed via SSE.
56
46
  */
57
- async function sendPrompt(baseUrl, sessionId, prompt, directory, model) {
47
+ async function sendPrompt(baseUrl, sessionId, prompt, directory, provider, model) {
58
48
  // Build request body
59
49
  const body = {
60
50
  parts: [{ type: "text", text: prompt }],
61
51
  };
62
- // Add model if provided and valid
63
- if (model) {
64
- const parsed = parseModel(model);
65
- if (parsed) {
66
- body.model = parsed;
67
- }
52
+ // Add model if provider and model are both provided
53
+ if (provider && model) {
54
+ body.model = { providerID: provider, modelID: model };
68
55
  }
69
56
  const response = await fetch(`${baseUrl}/session/${sessionId}/prompt_async`, {
70
57
  method: "POST",
package/dist/cli.js CHANGED
@@ -35,6 +35,7 @@ const program = new Command()
35
35
  .option("-a, --agent <id>", "Agent to use (required)")
36
36
  .option("-p, --prompt <text>", "Prompt text")
37
37
  .option("-m, --model <model>", "Model to use")
38
+ .option("--provider <provider>", "Provider for OpenCode (required for OpenCode)")
38
39
  .option("--allow <perms>", "Permission rules to allow (comma-separated)")
39
40
  .option("--deny <perms>", "Permission rules to deny (comma-separated)")
40
41
  .option("-f, --format <fmt>", "Output format: jsonl, tsv (default: tsv, truncated on TTY)")
@@ -50,6 +51,7 @@ Requirements:
50
51
 
51
52
  Examples:
52
53
  axexec -a claude "Refactor auth flow"
54
+ axexec -a opencode --provider anthropic -m claude-sonnet-4 "Hello"
53
55
  axexec -a claude -f jsonl "Audit deps" | jq 'select(.type=="tool.call")'
54
56
  axexec --list-agents | tail -n +2 | cut -f1
55
57
  `)
@@ -98,10 +100,36 @@ Examples:
98
100
  return;
99
101
  }
100
102
  }
103
+ // Validate --provider is only used with OpenCode
104
+ if (options.provider && options.agent !== "opencode") {
105
+ process.stderr.write("Error: --provider is only supported for OpenCode agent\n");
106
+ process.exitCode = 2;
107
+ return;
108
+ }
109
+ // Validate OpenCode requires --provider
110
+ if (options.agent === "opencode") {
111
+ // Check for deprecated provider/model format
112
+ if (options.model?.includes("/")) {
113
+ const [provider, model] = options.model.split("/", 2);
114
+ process.stderr.write("Error: Model format 'provider/model' is no longer supported\n");
115
+ process.stderr.write("Use separate --provider and --model flags:\n");
116
+ process.stderr.write(` axexec -a opencode --provider ${provider} -m ${model} ...\n`);
117
+ process.exitCode = 2;
118
+ return;
119
+ }
120
+ // Require --provider for OpenCode
121
+ if (!options.provider) {
122
+ process.stderr.write("Error: OpenCode requires --provider\n");
123
+ process.stderr.write(" axexec -a opencode --provider anthropic -m claude-sonnet-4 ...\n");
124
+ process.exitCode = 2;
125
+ return;
126
+ }
127
+ }
101
128
  // Run agent
102
129
  const result = await runAgent(options.agent, {
103
130
  prompt,
104
131
  model: options.model,
132
+ provider: options.provider,
105
133
  allow: options.allow,
106
134
  deny: options.deny,
107
135
  format,
@@ -13,6 +13,7 @@ async function executeAgent(agentId, adapter, options, configEnvironment, creden
13
13
  prompt: options.prompt,
14
14
  verbose: options.verbose ?? false,
15
15
  model: options.model,
16
+ provider: options.provider,
16
17
  rawLogPath: options.rawLog,
17
18
  debug: options.debug,
18
19
  configEnv: configEnvironment,
@@ -23,5 +23,5 @@ type ParseCredentialsResult = {
23
23
  *
24
24
  * Returns ok with undefined credentials if none are found.
25
25
  */
26
- declare function parseCredentialsFromEnvironment(agentId: AgentCli, model?: string): ParseCredentialsResult;
26
+ declare function parseCredentialsFromEnvironment(agentId: AgentCli): ParseCredentialsResult;
27
27
  export { parseCredentialsFromEnvironment };
@@ -15,7 +15,7 @@ import { getEnvironmentTrimmed } from "./credentials/get-environment-trimmed.js"
15
15
  *
16
16
  * Returns ok with undefined credentials if none are found.
17
17
  */
18
- function parseCredentialsFromEnvironment(agentId, model) {
18
+ function parseCredentialsFromEnvironment(agentId) {
19
19
  // Check for axexec credentials env var (full Credentials JSON)
20
20
  const credEnvironmentVariable = `AX_${agentId.toUpperCase()}_CREDENTIALS`;
21
21
  const credEnvironmentValue = process.env[credEnvironmentVariable];
@@ -51,12 +51,12 @@ function parseCredentialsFromEnvironment(agentId, model) {
51
51
  }
52
52
  }
53
53
  // Fall back to standard API key env vars
54
- return parseApiKeyFromEnvironment(agentId, model);
54
+ return parseApiKeyFromEnvironment(agentId);
55
55
  }
56
56
  /**
57
57
  * Parses API key from standard environment variables.
58
58
  */
59
- function parseApiKeyFromEnvironment(agentId, model) {
59
+ function parseApiKeyFromEnvironment(agentId) {
60
60
  let apiKey;
61
61
  switch (agentId) {
62
62
  case "claude": {
@@ -84,8 +84,8 @@ function parseApiKeyFromEnvironment(agentId, model) {
84
84
  break;
85
85
  }
86
86
  case "opencode": {
87
- // OpenCode can use multiple providers
88
- return parseOpenCodeApiKeyFromEnvironment(model);
87
+ // OpenCode requires AX_OPENCODE_CREDENTIALS - no env var fallback
88
+ return { ok: true, credentials: undefined };
89
89
  }
90
90
  case "copilot": {
91
91
  apiKey =
@@ -107,76 +107,4 @@ function parseApiKeyFromEnvironment(agentId, model) {
107
107
  },
108
108
  };
109
109
  }
110
- function normalizeOpenCodeProvider(provider) {
111
- if (!provider)
112
- return undefined;
113
- return provider === "gemini" ? "google" : provider;
114
- }
115
- function parseOpenCodeApiKeyFromEnvironment(model) {
116
- const providers = [
117
- {
118
- provider: "anthropic",
119
- envVar: "ANTHROPIC_API_KEY",
120
- value: getEnvironmentTrimmed("ANTHROPIC_API_KEY"),
121
- },
122
- {
123
- provider: "openai",
124
- envVar: "OPENAI_API_KEY",
125
- value: getEnvironmentTrimmed("OPENAI_API_KEY"),
126
- },
127
- {
128
- provider: "google",
129
- envVar: "GEMINI_API_KEY",
130
- value: getEnvironmentTrimmed("GEMINI_API_KEY"),
131
- },
132
- ];
133
- const available = providers.filter((entry) => entry.value !== undefined);
134
- const modelProvider = normalizeOpenCodeProvider(model?.split("/")[0]);
135
- if (modelProvider &&
136
- ["anthropic", "openai", "google"].includes(modelProvider)) {
137
- const match = providers.find((entry) => entry.provider === modelProvider);
138
- if (match?.value) {
139
- return {
140
- ok: true,
141
- credentials: {
142
- agent: "opencode",
143
- type: "api-key",
144
- provider: modelProvider,
145
- data: { apiKey: match.value },
146
- },
147
- };
148
- }
149
- const requiredEnvironmentVariable = match?.envVar ?? "AX_OPENCODE_CREDENTIALS";
150
- return {
151
- ok: false,
152
- exitCode: 2,
153
- message: `Error: ${requiredEnvironmentVariable} is required for OpenCode ` +
154
- `model provider "${modelProvider}".`,
155
- };
156
- }
157
- if (available.length === 0) {
158
- return { ok: true, credentials: undefined };
159
- }
160
- if (available.length === 1) {
161
- const match = available[0];
162
- if (!match)
163
- return { ok: true, credentials: undefined };
164
- return {
165
- ok: true,
166
- credentials: {
167
- agent: "opencode",
168
- type: "api-key",
169
- provider: match.provider,
170
- data: { apiKey: match.value },
171
- },
172
- };
173
- }
174
- const environmentList = available.map((entry) => entry.envVar).join(", ");
175
- return {
176
- ok: false,
177
- exitCode: 2,
178
- message: "Error: Multiple OpenCode API keys detected. " +
179
- `Set AX_OPENCODE_CREDENTIALS to disambiguate (${environmentList}).`,
180
- };
181
- }
182
110
  export { parseCredentialsFromEnvironment };
package/dist/run-agent.js CHANGED
@@ -35,10 +35,23 @@ async function runAgent(agentId, options) {
35
35
  };
36
36
  }
37
37
  const { adapter } = validation;
38
+ // Validate --model format for OpenCode (reject "provider/model" format)
39
+ if (validation.agentId === "opencode" && options.model?.includes("/")) {
40
+ const [provider, model] = options.model.split("/", 2);
41
+ diagnostics.error(`Error: Model format 'provider/model' is no longer supported.\n` +
42
+ `Use separate --provider and --model flags instead:\n` +
43
+ ` --provider ${provider} --model ${model}`);
44
+ setExitCode(2);
45
+ return {
46
+ events: [],
47
+ success: false,
48
+ execution: { agent: validation.agentId },
49
+ };
50
+ }
38
51
  // Use provided credentials or parse from environment
39
52
  const credentialsResult = options.credentials
40
53
  ? { ok: true, credentials: options.credentials }
41
- : parseCredentialsFromEnvironment(validation.agentId, options.model);
54
+ : parseCredentialsFromEnvironment(validation.agentId);
42
55
  if (!credentialsResult.ok) {
43
56
  diagnostics.error(credentialsResult.message);
44
57
  setExitCode(credentialsResult.exitCode);
@@ -24,9 +24,14 @@ interface RunOptions {
24
24
  * - Codex: model names ('o4-mini', 'gpt-5.1-codex')
25
25
  * - Gemini: aliases ('pro', 'flash') or full names ('gemini-2.5-pro')
26
26
  * - Copilot: model names ('gpt-5', 'claude-sonnet-4.5')
27
- * - OpenCode: provider/model format ('anthropic/claude-sonnet-4')
27
+ * - OpenCode: model name only ('claude-sonnet-4'), provider specified separately
28
28
  */
29
29
  model?: string;
30
+ /**
31
+ * Provider for multi-provider agents (e.g., OpenCode).
32
+ * OpenCode providers: 'anthropic', 'openai', 'google'
33
+ */
34
+ provider?: string;
30
35
  /**
31
36
  * Preserve GITHUB_SHA environment variable.
32
37
  *
@@ -17,6 +17,11 @@ interface RunAgentOptions {
17
17
  */
18
18
  credentials?: Credentials;
19
19
  model?: string;
20
+ /**
21
+ * Provider for multi-provider agents (e.g., OpenCode).
22
+ * Used to select which API key to use when multiple are present.
23
+ */
24
+ provider?: string;
20
25
  allow?: string;
21
26
  deny?: string;
22
27
  format?: OutputFormat;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "axexec",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "1.5.1",
5
+ "version": "1.6.0",
6
6
  "description": "Unified CLI runner for AI coding agents with normalized event streaming",
7
7
  "repository": {
8
8
  "type": "git",