claude-phone-local 2.3.2 → 2.3.4

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/.env.example CHANGED
@@ -54,6 +54,28 @@ DEFAULT_CALLER_ID=+15551234567
54
54
  # URL to your Claude API server (runs on your API server with Claude Max)
55
55
  CLAUDE_API_URL=http://10.0.0.200:3333
56
56
 
57
+ # Model the host-side wrapper (claude-api-server) asks Claude Code to use.
58
+ # Default if unset: claude-sonnet-5
59
+ # Set to a specific id for a custom / free API (OmniRoute, OpenRouter, etc.):
60
+ # CLAUDE_MODEL=gemini-2.5-flash
61
+ # CLAUDE_MODEL=deepseek-chat
62
+ # CLAUDE_MODEL=claude-3-5-haiku-20241022
63
+ # Or omit the flag entirely so Claude Code / OmniRoute uses its own default:
64
+ # CLAUDE_MODEL=default
65
+ # CLAUDE_MODEL=claude-sonnet-5
66
+
67
+ # Seconds before giving up on a phone-turn answer (default 180)
68
+ # CLAUDE_TIMEOUT=180
69
+
70
+ # ---- Custom / free API proxy (OmniRoute, OpenRouter, LiteLLM, etc.) ----
71
+ # When ANTHROPIC_BASE_URL is set, claude-api-server KEEPS ANTHROPIC_API_KEY
72
+ # so the proxy can authenticate. Without a custom base URL the key is stripped
73
+ # so Claude Code uses your Claude subscription login instead.
74
+ # ANTHROPIC_BASE_URL=https://your-omniroute-host/v1
75
+ # ANTHROPIC_API_KEY=your-proxy-key
76
+ # If the key must be kept even without ANTHROPIC_BASE_URL:
77
+ # CLAUDE_USE_API_KEY=1
78
+
57
79
  # ====================================
58
80
  # Speech-to-text / Text-to-speech mode
59
81
  # ====================================
package/README.md CHANGED
@@ -36,6 +36,12 @@ Manual, one time:
36
36
  - **Claude Code CLI**, logged in — `claude --version`
37
37
  - **Node.js 18+** on the host, for `claude-api-server`
38
38
  - A **SIP extension** on 3CX (or any SIP PBX)
39
+ - **3CX Session Border Controller (SBC)** installed on this PC — required
40
+ unless this machine is already on the same LAN as your 3CX PBX with no
41
+ NAT/firewall in between. Most setups need it. `claude-phone setup` detects
42
+ it automatically and configures around it (moves drachtio to port 5070 to
43
+ avoid the SBC's port 5060). Full instructions: [docs/SETUP.md § Install the
44
+ 3CX SBC](docs/SETUP.md#2-install-the-3cx-sbc-only-if-needed).
39
45
 
40
46
  Everything else — speech models, voices, device config — is downloaded and
41
47
  generated automatically on first run.
@@ -87,7 +93,10 @@ In a second terminal, start the host-side Claude wrapper:
87
93
 
88
94
  ```bash
89
95
  cd claude-api-server
90
- CLAUDE_MODEL=claude-sonnet-5 node server.js
96
+ # Default model is claude-sonnet-5. For OmniRoute / a custom API:
97
+ # CLAUDE_MODEL=default # let Claude Code pick
98
+ # CLAUDE_MODEL=gemini-2.5-flash # or whatever your proxy supports
99
+ node server.js
91
100
  ```
92
101
 
93
102
  When you see this, call your extension:
@@ -73,9 +73,6 @@
73
73
  "cpu": [
74
74
  "arm64"
75
75
  ],
76
- "libc": [
77
- "glibc"
78
- ],
79
76
  "license": "SEE LICENSE IN LICENSE.md",
80
77
  "optional": true,
81
78
  "os": [
@@ -89,9 +86,6 @@
89
86
  "cpu": [
90
87
  "arm64"
91
88
  ],
92
- "libc": [
93
- "musl"
94
- ],
95
89
  "license": "SEE LICENSE IN LICENSE.md",
96
90
  "optional": true,
97
91
  "os": [
@@ -105,9 +99,6 @@
105
99
  "cpu": [
106
100
  "x64"
107
101
  ],
108
- "libc": [
109
- "glibc"
110
- ],
111
102
  "license": "SEE LICENSE IN LICENSE.md",
112
103
  "optional": true,
113
104
  "os": [
@@ -121,9 +112,6 @@
121
112
  "cpu": [
122
113
  "x64"
123
114
  ],
124
- "libc": [
125
- "musl"
126
- ],
127
115
  "license": "SEE LICENSE IN LICENSE.md",
128
116
  "optional": true,
129
117
  "os": [
@@ -106,23 +106,18 @@ function buildClaudeEnvironment() {
106
106
  CLAUDE_CODE_ENTRYPOINT: 'cli',
107
107
  };
108
108
 
109
- // CRITICAL: Remove ANTHROPIC_API_KEY so Claude CLI uses subscription auth
110
- // If ANTHROPIC_API_KEY is set (even to placeholder), CLI tries API auth instead
111
- delete env.ANTHROPIC_API_KEY;
109
+ // CRITICAL: Only remove ANTHROPIC_API_KEY when NOT using custom API proxy
110
+ // If ANTHROPIC_BASE_URL or CLAUDE_USE_API_KEY is set, keep the key for proxy auth
111
+ // Otherwise delete it so Claude CLI uses subscription auth
112
+ if (!env.ANTHROPIC_BASE_URL && !env.CLAUDE_USE_API_KEY) {
113
+ delete env.ANTHROPIC_API_KEY;
114
+ }
112
115
 
113
116
  return env;
114
117
  }
115
118
 
116
119
  // Pre-build the environment once at startup
117
120
  const claudeEnv = buildClaudeEnvironment();
118
- console.log('[STARTUP] Loaded environment with', Object.keys(claudeEnv).length, 'variables');
119
- console.log('[STARTUP] PATH includes:', claudeEnv.PATH.split(':').slice(0, 5).join(', '), '...');
120
-
121
- // Log which API keys are available (without showing values)
122
- const apiKeys = Object.keys(claudeEnv).filter(k =>
123
- k.includes('API_KEY') || k.includes('TOKEN') || k.includes('SECRET') || k === 'PAI_DIR'
124
- );
125
- console.log('[STARTUP] API keys loaded:', apiKeys.join(', '));
126
121
 
127
122
  // Every phone turn used to spawn a fresh CLI process. Without
128
123
  // --strict-mcp-config it tries to connect to every configured MCP server
@@ -132,8 +127,40 @@ console.log('[STARTUP] API keys loaded:', apiKeys.join(', '));
132
127
  // available over the phone.
133
128
  const STRICT_MCP = process.env.PHONE_ENABLE_MCP !== '1';
134
129
 
135
- // Model selection - Sonnet for balanced speed/quality
136
- const CLAUDE_MODEL = process.env.CLAUDE_MODEL || 'claude-sonnet-5';
130
+ /**
131
+ * Resolve the Claude model to use.
132
+ *
133
+ * OmniRoute / custom API proxies often don't support `claude-sonnet-5`.
134
+ * Set CLAUDE_MODEL to the proxy's model id, or to "" / "default" / "none"
135
+ * to omit --model entirely and let Claude Code / OmniRoute pick its own.
136
+ *
137
+ * Request bodies may also pass `model` to override per-call.
138
+ */
139
+ function resolveClaudeModel(requestModel) {
140
+ const fallback = (process.env.ANTHROPIC_BASE_URL || process.env.CLAUDE_USE_API_KEY) ? 'default' : 'claude-sonnet-5';
141
+ const raw = (requestModel !== undefined && requestModel !== null && String(requestModel).trim() !== '')
142
+ ? String(requestModel).trim()
143
+ : (process.env.CLAUDE_MODEL !== undefined ? process.env.CLAUDE_MODEL : fallback);
144
+ const value = String(raw).trim();
145
+ if (!value || value.toLowerCase() === 'default' || value.toLowerCase() === 'none') {
146
+ return null; // omit --model / SDK model option
147
+ }
148
+ return value;
149
+ }
150
+
151
+ const CLAUDE_MODEL = resolveClaudeModel();
152
+
153
+ console.log('[STARTUP] Loaded environment with', Object.keys(claudeEnv).length, 'variables');
154
+ console.log('[STARTUP] PATH includes:', claudeEnv.PATH.split(':').slice(0, 5).join(', '), '...');
155
+
156
+ // Log which API keys are available (without showing values)
157
+ const apiKeys = Object.keys(claudeEnv).filter(k =>
158
+ k.includes('API_KEY') || k.includes('TOKEN') || k.includes('SECRET') || k === 'PAI_DIR'
159
+ );
160
+ console.log('[STARTUP] API keys loaded:', apiKeys.join(', '));
161
+ console.log('[STARTUP] Claude model:', CLAUDE_MODEL || 'default (Claude Code / OmniRoute)');
162
+ console.log('[STARTUP] ANTHROPIC_BASE_URL:', claudeEnv.ANTHROPIC_BASE_URL || '(not set — subscription auth)');
163
+ console.log('[STARTUP] ANTHROPIC_API_KEY:', claudeEnv.ANTHROPIC_API_KEY ? 'kept (proxy auth)' : 'stripped (subscription auth)');
137
164
 
138
165
  /**
139
166
  * Voice Context - Prepended to all voice queries
@@ -194,21 +221,25 @@ Example response:
194
221
  const callSessions = new Map();
195
222
 
196
223
  class CallSession {
197
- constructor(callId) {
224
+ constructor(callId, model = CLAUDE_MODEL) {
198
225
  this.callId = callId;
226
+ this.model = model;
199
227
  this._queue = [];
200
228
  this._queueWaiters = [];
201
229
  this._ended = false;
202
230
  this._pendingResultResolvers = [];
203
231
 
232
+ const options = {
233
+ permissionMode: 'bypassPermissions',
234
+ allowDangerouslySkipPermissions: true,
235
+ strictMcpConfig: STRICT_MCP,
236
+ };
237
+ // Omit model so Claude Code / OmniRoute uses its own default
238
+ if (model) options.model = model;
239
+
204
240
  this.query = query({
205
241
  prompt: this._messageGenerator(),
206
- options: {
207
- model: CLAUDE_MODEL,
208
- permissionMode: 'bypassPermissions',
209
- allowDangerouslySkipPermissions: true,
210
- strictMcpConfig: STRICT_MCP,
211
- },
242
+ options,
212
243
  });
213
244
 
214
245
  this._consumeLoop().catch((err) => {
@@ -276,12 +307,12 @@ class CallSession {
276
307
  }
277
308
  }
278
309
 
279
- function getOrCreateSession(callId) {
310
+ function getOrCreateSession(callId, model = CLAUDE_MODEL) {
280
311
  let session = callSessions.get(callId);
281
312
  if (!session) {
282
- session = new CallSession(callId);
313
+ session = new CallSession(callId, model);
283
314
  callSessions.set(callId, session);
284
- console.log(`[${new Date().toISOString()}] SDK session started: ${callId}`);
315
+ console.log(`[${new Date().toISOString()}] SDK session started: ${callId} (model=${model || 'default'})`);
285
316
  }
286
317
  return session;
287
318
  }
@@ -321,15 +352,16 @@ function parseClaudeStdout(stdout) {
321
352
  // Session storage for the one-shot /ask-structured path only.
322
353
  const structuredSessions = new Map();
323
354
 
324
- function runClaudeOnce({ fullPrompt, callId, timestamp }) {
355
+ function runClaudeOnce({ fullPrompt, callId, timestamp, model = CLAUDE_MODEL }) {
325
356
  const startTime = Date.now();
326
357
 
327
358
  const args = [
328
359
  '--dangerously-skip-permissions',
329
360
  ...(STRICT_MCP ? ['--strict-mcp-config'] : []),
330
361
  '-p', fullPrompt,
331
- '--model', CLAUDE_MODEL
332
362
  ];
363
+ // Omit --model so Claude Code / OmniRoute uses its own default
364
+ if (model) args.push('--model', model);
333
365
 
334
366
  if (callId) {
335
367
  if (structuredSessions.has(callId)) {
@@ -384,7 +416,8 @@ app.use((req, res, next) => {
384
416
  * {
385
417
  * "prompt": "What Docker containers are running?",
386
418
  * "callId": "optional-call-uuid",
387
- * "devicePrompt": "optional device-specific prompt"
419
+ * "devicePrompt": "optional device-specific prompt",
420
+ * "model": "optional model override (or \"default\" to omit --model)"
388
421
  * }
389
422
  *
390
423
  * Response:
@@ -401,9 +434,10 @@ app.use((req, res, next) => {
401
434
  * - This allows each device (NAS, Proxmox, etc.) to have its own identity and skills
402
435
  */
403
436
  app.post('/ask', async (req, res) => {
404
- const { prompt, callId, devicePrompt } = req.body;
437
+ const { prompt, callId, devicePrompt, model: requestModel } = req.body;
405
438
  const startTime = Date.now();
406
439
  const timestamp = new Date().toISOString();
440
+ const model = resolveClaudeModel(requestModel);
407
441
 
408
442
  if (!prompt) {
409
443
  return res.status(400).json({
@@ -413,7 +447,7 @@ app.post('/ask', async (req, res) => {
413
447
  }
414
448
 
415
449
  console.log(`[${timestamp}] QUERY: "${prompt.substring(0, 100)}..."`);
416
- console.log(`[${timestamp}] MODEL: ${CLAUDE_MODEL}`);
450
+ console.log(`[${timestamp}] MODEL: ${model || 'default (Claude Code / OmniRoute)'}`);
417
451
  console.log(`[${timestamp}] SESSION: callId=${callId || 'none'}, existing=${callId ? callSessions.has(callId) : false}`);
418
452
  console.log(`[${timestamp}] DEVICE PROMPT: ${devicePrompt ? 'Yes (' + devicePrompt.substring(0, 30) + '...)' : 'No'}`);
419
453
 
@@ -429,7 +463,7 @@ app.post('/ask', async (req, res) => {
429
463
  * every turn would just be redundant tokens (the CLI's --resume worked
430
464
  * the same way: system framing lived in turn 1's prompt).
431
465
  */
432
- const session = callId ? getOrCreateSession(callId) : null;
466
+ const session = callId ? getOrCreateSession(callId, model) : null;
433
467
 
434
468
  let fullPrompt = '';
435
469
  if (!session || session._sentContext !== true) {
@@ -441,7 +475,7 @@ app.post('/ask', async (req, res) => {
441
475
  }
442
476
  fullPrompt += prompt;
443
477
 
444
- const activeSession = session || getOrCreateSession(`__oneshot_${startTime}_${Math.random().toString(36).slice(2)}`);
478
+ const activeSession = session || getOrCreateSession(`__oneshot_${startTime}_${Math.random().toString(36).slice(2)}`, model);
445
479
 
446
480
  const result = await activeSession.sendMessage(fullPrompt);
447
481
 
@@ -512,9 +546,11 @@ app.post('/ask-structured', async (req, res) => {
512
546
  schema = {},
513
547
  includeVoiceContext = false,
514
548
  maxRetries = 1,
549
+ model: requestModel,
515
550
  } = req.body || {};
516
551
 
517
552
  const timestamp = new Date().toISOString();
553
+ const model = resolveClaudeModel(requestModel);
518
554
 
519
555
  if (!prompt) {
520
556
  return res.status(400).json({ success: false, error: 'Missing prompt in request body' });
@@ -535,7 +571,7 @@ app.post('/ask-structured', async (req, res) => {
535
571
  });
536
572
 
537
573
  console.log(`[${timestamp}] STRUCTURED QUERY: "${String(prompt).substring(0, 100)}..."`);
538
- console.log(`[${timestamp}] MODEL: ${CLAUDE_MODEL}`);
574
+ console.log(`[${timestamp}] MODEL: ${model || 'default (Claude Code / OmniRoute)'}`);
539
575
  console.log(`[${timestamp}] SESSION: callId=${callId || 'none'}, existing=${callId ? (structuredSessions.has(callId) ? 'yes' : 'no') : 'none'}`);
540
576
 
541
577
  try {
@@ -547,7 +583,7 @@ app.post('/ask-structured', async (req, res) => {
547
583
 
548
584
  for (let attempt = 0; attempt <= retries; attempt++) {
549
585
  attemptsMade = attempt + 1;
550
- const { code, stdout, stderr, duration_ms } = await runClaudeOnce({ fullPrompt, callId, timestamp });
586
+ const { code, stdout, stderr, duration_ms } = await runClaudeOnce({ fullPrompt, callId, timestamp, model });
551
587
  totalDuration += duration_ms;
552
588
 
553
589
  if (code !== 0) {
@@ -652,6 +688,8 @@ app.get('/health', (req, res) => {
652
688
  res.json({
653
689
  status: 'ok',
654
690
  service: 'claude-api-server',
691
+ model: CLAUDE_MODEL || 'default',
692
+ proxyAuth: Boolean(claudeEnv.ANTHROPIC_BASE_URL || claudeEnv.CLAUDE_USE_API_KEY),
655
693
  timestamp: new Date().toISOString()
656
694
  });
657
695
  });