thinkpool-pair 0.7.180 → 0.7.181

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/byok-detect.mjs CHANGED
@@ -1,16 +1,24 @@
1
1
  /* ─────────────────────────────────────────────────────────────
2
2
  byok-detect — "paste a key → pick a model" for the bridge menu.
3
- ThinkPool Code is structured-only (Anthropic Messages API), so only
4
- endpoints that speak that format work directly. OpenRouter is the
5
- universal path: one key, any model, an Anthropic-compatible endpoint.
3
+ ThinkPool Code is structured-only (Anthropic Messages API). That format
4
+ has become the de-facto standard for agentic coding: Anthropic, Z.ai/GLM,
5
+ DeepSeek, Moonshot/Kimi and MiniMax all serve it natively (each base
6
+ below live-probed 2026-07-10: POST <base>/v1/messages → 401 auth error,
7
+ i.e. the path exists), and LM Studio / llama.cpp serve it on localhost.
8
+ The holdouts are OpenAI + Google — those need a translation gateway
9
+ (LiteLLM etc.). NOTE OpenRouter's native /v1/messages serves ANTHROPIC
10
+ MODELS ONLY (live-probed 2026-07-09, see src/lib/providers.js) — it is
11
+ not a GPT/Gemini path, despite what this file used to claim.
6
12
  A bare OpenAI/Gemini/Groq key is DETECTED so we can guide the user to
7
- OpenRouter or a gateway instead of failing silently.
13
+ a gateway instead of failing silently.
8
14
  Spec: docs/specs/2026-06-28-byok-only-two-tier-pivot.md (addendum)
9
15
  ───────────────────────────────────────────────────────────── */
10
16
 
11
17
  // Providers whose endpoint speaks the Anthropic Messages API → usable directly
12
18
  // in structured mode. `base` is what we set as ANTHROPIC_BASE_URL; `models` is
13
19
  // the model-list endpoint; `auth` is how the model-list call authenticates.
20
+ // A wrong/moved `models` URL degrades gracefully — fetchModels() returns []
21
+ // and the menu falls back to a free-text model prompt.
14
22
  export const ANTHROPIC_COMPATIBLE = {
15
23
  anthropic: {
16
24
  name: 'Anthropic',
@@ -19,16 +27,45 @@ export const ANTHROPIC_COMPATIBLE = {
19
27
  auth: 'x-api-key',
20
28
  },
21
29
  openrouter: {
22
- name: 'OpenRouter — any model',
30
+ name: 'OpenRouter (Anthropic models)',
23
31
  base: 'https://openrouter.ai/api',
24
32
  models: 'https://openrouter.ai/api/v1/models',
25
33
  auth: 'bearer',
26
34
  },
35
+ zai: {
36
+ name: 'Z.ai — GLM',
37
+ base: 'https://api.z.ai/api/anthropic',
38
+ models: 'https://api.z.ai/api/paas/v4/models',
39
+ auth: 'bearer',
40
+ },
41
+ deepseek: {
42
+ name: 'DeepSeek',
43
+ base: 'https://api.deepseek.com/anthropic',
44
+ models: 'https://api.deepseek.com/models',
45
+ auth: 'bearer',
46
+ },
47
+ moonshot: {
48
+ name: 'Moonshot — Kimi',
49
+ base: 'https://api.moonshot.ai/anthropic',
50
+ models: 'https://api.moonshot.ai/v1/models',
51
+ auth: 'bearer',
52
+ },
53
+ minimax: {
54
+ name: 'MiniMax',
55
+ base: 'https://api.minimax.io/anthropic',
56
+ models: 'https://api.minimax.io/v1/models',
57
+ auth: 'bearer',
58
+ },
27
59
  }
28
60
 
29
61
  // Key-prefix → provider. `compatible: true` means it works directly; false
30
- // means we detected it but it needs OpenRouter or a gateway (different wire
31
- // format). Keep the most specific prefixes first (sk-ant- / sk-or- before sk-).
62
+ // means we detected it but it needs a gateway (different wire format).
63
+ // Keep the most specific prefixes first (sk-ant- / sk-or- before sk-).
64
+ // Undetectable-by-prefix but directly compatible: DeepSeek + Moonshot keys are
65
+ // bare `sk-…` (indistinguishable from OpenAI — gatewayHint names the escape),
66
+ // Z.ai keys are `<hex>.<secret>` with no stable prefix. Those route via the
67
+ // dashboard's provider presets (src/lib/providers.js BRIDGE_PROVIDER_PRESETS)
68
+ // or the manual-endpoint path instead.
32
69
  const PREFIXES = [
33
70
  ['sk-ant-', 'anthropic', true, 'Anthropic'],
34
71
  ['sk-or-', 'openrouter', true, 'OpenRouter'],
@@ -77,6 +114,13 @@ export async function fetchModels({ modelsUrl, key, auth }) {
77
114
  }
78
115
 
79
116
  // The friendly message shown when a detected key isn't directly usable.
117
+ // Honest scope (2026-07-10): OpenRouter's native /v1/messages is Anthropic-
118
+ // models-only, so it is NOT the "any model" escape hatch for a GPT/Gemini key —
119
+ // a translation gateway is. And a bare sk- key might not be OpenAI at all:
120
+ // DeepSeek and Moonshot/Kimi keys share the prefix and DO work directly.
80
121
  export function gatewayHint(name) {
81
- return `That key looks like ${name}. ThinkPool Code runs on the Anthropic Messages API, so it can't use that key directly. Easiest fix: paste an OpenRouter key (sk-or-…) — one key, every model (GPT, Gemini, Llama, and more). Or point it at your own gateway (e.g. LiteLLM).`
122
+ const skAmbiguity = name === 'OpenAI'
123
+ ? ' (If that key is actually DeepSeek or Moonshot/Kimi — they also start with sk- — it works directly: pick that provider in the dashboard’s LLM providers panel instead.)'
124
+ : ''
125
+ return `That key looks like ${name}. ThinkPool Code runs on the Anthropic Messages API — the format Claude, GLM, DeepSeek, Kimi, MiniMax and local models (LM Studio, llama.cpp) all speak natively — but that provider doesn't, so the key can't be used directly. Route it through your own gateway (e.g. LiteLLM) instead.${skAmbiguity}`
82
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.180",
3
+ "version": "0.7.181",
4
4
  "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
5
5
  "type": "module",
6
6
  "bin": {
package/providers.mjs CHANGED
@@ -151,12 +151,30 @@ export function unseal(sealedB64) {
151
151
  }
152
152
 
153
153
  // ── CRUD ────────────────────────────────────────────────────────────────
154
+ /* Local-LLM exception (2026-07-10): a provider on the SAME machine (LM Studio,
155
+ llama.cpp — both speak the Anthropic Messages API natively) is allowed over
156
+ plain http and without a key. Both relaxations are LOOPBACK-ONLY:
157
+ • https-only stays for every remote host — a key on plaintext http across a
158
+ real wire is the leak the rule exists to stop; on 127.0.0.1 there is no wire.
159
+ • key-required stays for every remote host — local servers simply don't take
160
+ one (resolveProviderEnv synthesizes a placeholder token so the SDK doesn't
161
+ fall back to the host's Claude login against a localhost URL). */
162
+ export function isLoopbackHost(hostname) {
163
+ const h = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, '')
164
+ // 127/8 must be a LITERAL IPv4 — '127.evil.io' is a DNS name that can resolve
165
+ // anywhere, so a bare startsWith('127.') would be a spoofable hole.
166
+ return h === 'localhost' || h === '::1' || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)
167
+ }
168
+
154
169
  function validate({ name, baseUrl, key }) {
155
170
  if (!name || typeof name !== 'string' || !name.trim()) return 'name is required'
156
- if (!key || typeof key !== 'string' || !key.trim()) return 'key is required'
157
171
  let u
158
172
  try { u = new URL(String(baseUrl)) } catch { return 'baseUrl must be a valid https url' }
159
- if (u.protocol !== 'https:') return 'baseUrl must be https'
173
+ const local = isLoopbackHost(u.hostname)
174
+ if (u.protocol !== 'https:' && !(u.protocol === 'http:' && local)) {
175
+ return 'baseUrl must be https (plain http is allowed only for localhost)'
176
+ }
177
+ if (!local && (!key || typeof key !== 'string' || !key.trim())) return 'key is required'
160
178
  return null
161
179
  }
162
180
 
@@ -174,7 +192,7 @@ export function addProvider(fields) {
174
192
  name: String(fields.name).trim(),
175
193
  baseUrl: String(fields.baseUrl).trim(),
176
194
  model: fields.model ? String(fields.model).trim() : undefined,
177
- key: String(fields.key).trim(),
195
+ key: fields.key ? String(fields.key).trim() : '', // '' = keyless local provider
178
196
  addedAt: Date.now(),
179
197
  })
180
198
  saveProviders(arr)
@@ -344,7 +362,10 @@ export function resolveProviderEnv(id) {
344
362
  if (!id || id === BUILTIN_ID) return null
345
363
  const p = loadProviders().find((x) => x.id === id)
346
364
  if (!p) return null
347
- const env = { ANTHROPIC_BASE_URL: p.baseUrl, ANTHROPIC_AUTH_TOKEN: p.key }
365
+ // Keyless (loopback) provider: the SDK still needs a non-empty token, or it
366
+ // falls back to the host's Claude login against the localhost URL. Local
367
+ // servers (LM Studio / llama.cpp) accept any bearer value.
368
+ const env = { ANTHROPIC_BASE_URL: p.baseUrl, ANTHROPIC_AUTH_TOKEN: p.key || 'thinkpool-local' }
348
369
  if (p.model) env.ANTHROPIC_MODEL = p.model
349
370
  return env
350
371
  }