polyclaude 0.4.2__tar.gz → 0.4.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: polyclaude
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Use OpenAI, Gemini, and any OpenAI-compatible model inside the Claude Code CLI.
5
5
  Project-URL: Homepage, https://github.com/ibedevesh/polyclaude
6
6
  Project-URL: Repository, https://github.com/ibedevesh/polyclaude
@@ -86,6 +86,7 @@ polyclaude --gemini -- -p "explain this repo"
86
86
  | OpenAI | `--openai` | `OPENAI_API_KEY` | `gpt-4.1` |
87
87
  | Moonshot Kimi | `--kimi` (or `--moonshot`) | `MOONSHOT_API_KEY` | `kimi-k3.1-pro` |
88
88
  | DeepSeek | `--deepseek` | `DEEPSEEK_API_KEY` | `deepseek-flash` |
89
+ | Z.ai GLM | `--glm` (or `--zai`) | `ZAI_API_KEY` | `glm-5.3` |
89
90
  | Groq | `--groq` | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
90
91
  | OpenRouter | `--openrouter` | `OPENROUTER_API_KEY` | `anthropic/claude-3.5-sonnet` |
91
92
  | Ollama (local) | `--ollama` | — | `qwen2.5-coder` |
@@ -111,6 +112,8 @@ calling and reasoning keep working:
111
112
  `kimi-k2.6`) work via `--model`.
112
113
  - Reasoning models reject non-default sampling params — dropped automatically.
113
114
  - Turns that hit the output-token cap are transparently continued.
115
+ - A deep-thinking turn on a long context can take minutes, so the bridge waits
116
+ up to 600s for the backend. Raise or lower it with `--timeout SECONDS`.
114
117
 
115
118
  Run `polyclaude --list` to see everything.
116
119
 
@@ -65,6 +65,7 @@ polyclaude --gemini -- -p "explain this repo"
65
65
  | OpenAI | `--openai` | `OPENAI_API_KEY` | `gpt-4.1` |
66
66
  | Moonshot Kimi | `--kimi` (or `--moonshot`) | `MOONSHOT_API_KEY` | `kimi-k3.1-pro` |
67
67
  | DeepSeek | `--deepseek` | `DEEPSEEK_API_KEY` | `deepseek-flash` |
68
+ | Z.ai GLM | `--glm` (or `--zai`) | `ZAI_API_KEY` | `glm-5.3` |
68
69
  | Groq | `--groq` | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
69
70
  | OpenRouter | `--openrouter` | `OPENROUTER_API_KEY` | `anthropic/claude-3.5-sonnet` |
70
71
  | Ollama (local) | `--ollama` | — | `qwen2.5-coder` |
@@ -90,6 +91,8 @@ calling and reasoning keep working:
90
91
  `kimi-k2.6`) work via `--model`.
91
92
  - Reasoning models reject non-default sampling params — dropped automatically.
92
93
  - Turns that hit the output-token cap are transparently continued.
94
+ - A deep-thinking turn on a long context can take minutes, so the bridge waits
95
+ up to 600s for the backend. Raise or lower it with `--timeout SECONDS`.
93
96
 
94
97
  Run `polyclaude --list` to see everything.
95
98
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "polyclaude"
7
- version = "0.4.2"
7
+ version = "0.4.4"
8
8
  description = "Use OpenAI, Gemini, and any OpenAI-compatible model inside the Claude Code CLI."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,3 +1,3 @@
1
1
  """polyclaude — use OpenAI, Gemini, and any OpenAI-compatible model in Claude Code."""
2
2
 
3
- __version__ = "0.4.2"
3
+ __version__ = "0.4.4"
@@ -17,6 +17,7 @@ Configured entirely via POLYCLAUDE_* environment variables (set by the CLI):
17
17
  OpenAI uses it as-is, Kimi maps it to low|high|max
18
18
  POLYCLAUDE_AUTOCONTINUE 1 = stitch a turn truncated at the token cap
19
19
  POLYCLAUDE_MAXTOK output cap (default 32768)
20
+ POLYCLAUDE_TIMEOUT seconds to wait for a backend turn (default 600)
20
21
  POLYCLAUDE_SYSTEM_APPEND_FILE append this file to the main system prompt
21
22
  POLYCLAUDE_SYSTEM_REPLACE_FILE replace the main system prompt with this file
22
23
  POLYCLAUDE_LOG optional path to a wire log
@@ -37,6 +38,10 @@ REASONING = os.environ.get("POLYCLAUDE_REASONING", "high")
37
38
  AUTOCONT = os.environ.get("POLYCLAUDE_AUTOCONTINUE", "1") == "1"
38
39
  MAX_TOK = int(os.environ.get("POLYCLAUDE_MAXTOK", "32768"))
39
40
  MAXCONT = int(os.environ.get("POLYCLAUDE_MAXCONT", "4"))
41
+ # How long to wait for the backend to finish a turn. Thinking models (Kimi at
42
+ # reasoning_effort=max, GPT-5.x, Gemini 3.x) routinely need minutes on a long
43
+ # context, and the old hard-coded 120s cut them off mid-answer.
44
+ TIMEOUT = int(os.environ.get("POLYCLAUDE_TIMEOUT", "600"))
40
45
  LOG = os.environ.get("POLYCLAUDE_LOG", "")
41
46
 
42
47
  # Moonshot (Kimi) is OpenAI-compatible but has three quirks:
@@ -51,6 +56,15 @@ KIMI = "moonshot.ai" in BASE or "kimi.ai" in BASE
51
56
  # polyclaude's low|medium|high -> Kimi's low|high|max (it has no "medium")
52
57
  _KIMI_EFFORT = {"low": "low", "medium": "high", "high": "max"}
53
58
 
59
+ # Z.ai (GLM) is OpenAI-compatible but, like Kimi, reads `max_tokens` rather than
60
+ # `max_completion_tokens`. GLM-5.3 always thinks (thinking.type can only be
61
+ # "enabled") and exposes the same low|high|max effort ladder as Kimi — z.ai
62
+ # recommends "max" for coding. Sampling is left alone for the thinking models
63
+ # for the same reason as Kimi: the docs warn against pinning temperature and
64
+ # top_p together, and Claude Code sends both.
65
+ GLM = "api.z.ai" in BASE or "bigmodel.cn" in BASE
66
+ _GLM_EFFORT = _KIMI_EFFORT
67
+
54
68
  # --- identity scrub / inspect -------------------------------------------------
55
69
  # POLYCLAUDE_SCRUB=1 enable rewriting of the outgoing payload
56
70
  # POLYCLAUDE_IDENTITY_MAP "old=new,old2=new2" literal replacements, applied
@@ -349,9 +363,9 @@ def _to_openai(body):
349
363
  tgt = _pick_model(body.get("model", ""))
350
364
  cap = min(int(body.get("max_tokens", MAX_TOK)), MAX_TOK)
351
365
  out = {"model": tgt, "messages": msgs}
352
- # Kimi reads max_tokens (and bills reasoning tokens against it); the
353
- # OpenAI family wants max_completion_tokens.
354
- out["max_tokens" if KIMI else "max_completion_tokens"] = cap
366
+ # Kimi reads max_tokens (and bills reasoning tokens against it), and so does
367
+ # z.ai; the OpenAI family wants max_completion_tokens.
368
+ out["max_tokens" if (KIMI or GLM) else "max_completion_tokens"] = cap
355
369
  # reasoning models (and Kimi, whose sampling is fixed server-side) reject
356
370
  # explicit temperature / top_p
357
371
  reasoning = KIMI or tgt.startswith(("gpt-5", "gpt-6", "o1", "o3", "o4"))
@@ -359,6 +373,11 @@ def _to_openai(body):
359
373
  # K3.1 Pro always thinks; only the depth is tunable. Older Kimi models
360
374
  # don't take the field at all.
361
375
  out["reasoning_effort"] = _KIMI_EFFORT.get(REASONING, "max")
376
+ if GLM and tgt.startswith("glm-5"):
377
+ # GLM-5.x thinks by default; only the depth is tunable. The -flash tiers
378
+ # accept the field too, so key off the family rather than the exact id.
379
+ out["reasoning_effort"] = _GLM_EFFORT.get(REASONING, "max")
380
+ reasoning = True
362
381
  if not reasoning:
363
382
  if body.get("temperature") is not None:
364
383
  out["temperature"] = body["temperature"]
@@ -466,7 +485,8 @@ def _use_responses(tgt):
466
485
  # ---------------------------------------------------------------------------
467
486
  # HTTP to the backend
468
487
  # ---------------------------------------------------------------------------
469
- def _post(path, payload, timeout):
488
+ def _post(path, payload, timeout=None):
489
+ timeout = TIMEOUT if timeout is None else timeout
470
490
  req = urllib.request.Request(
471
491
  f"{BASE}/{path}", data=json.dumps(payload).encode(),
472
492
  headers={"Authorization": f"Bearer {API_KEY}",
@@ -479,7 +499,7 @@ def _post(path, payload, timeout):
479
499
 
480
500
 
481
501
  def _chat(payload):
482
- oai = _post("chat/completions", payload, 120)
502
+ oai = _post("chat/completions", payload)
483
503
  if not AUTOCONT:
484
504
  return oai
485
505
  for _ in range(MAXCONT):
@@ -493,7 +513,7 @@ def _chat(payload):
493
513
  half["reasoning_content"] = msg["reasoning_content"]
494
514
  cont["messages"] = payload["messages"] + [
495
515
  half, {"role": "user", "content": "continue"}]
496
- nxt = _post("chat/completions", cont, 120)
516
+ nxt = _post("chat/completions", cont)
497
517
  nmsg = (nxt.get("choices") or [{}])[0].get("message", {})
498
518
  oai["choices"][0]["message"]["content"] = \
499
519
  (msg.get("content") or "") + (nmsg.get("content") or "")
@@ -803,7 +823,7 @@ class Bridge:
803
823
  f"msgs={len(body.get('messages', []))} ===")
804
824
  try:
805
825
  if _use_responses(tgt):
806
- oai = _responses_to_oai(_post("responses", _to_responses(body), 300))
826
+ oai = _responses_to_oai(_post("responses", _to_responses(body)))
807
827
  _log(f">>> via /responses (reasoning={REASONING})")
808
828
  else:
809
829
  oai = _chat(_to_openai(body))
@@ -816,8 +836,18 @@ class Bridge:
816
836
  _log(f"!!! backend {e.code}: {detail}")
817
837
  sse = _error_sse(model, f"backend {e.code}: {detail}")
818
838
  except Exception as e:
819
- _log(f"!!! {e!r}")
820
- sse = _error_sse(model, repr(e))
839
+ # a read timeout surfaces as TimeoutError; a connect timeout comes
840
+ # wrapped in URLError(reason=TimeoutError)
841
+ if isinstance(e, TimeoutError) or isinstance(
842
+ getattr(e, "reason", None), TimeoutError):
843
+ _log(f"!!! timeout after {TIMEOUT}s: {e!r}")
844
+ sse = _error_sse(model, (
845
+ f"the backend did not answer within {TIMEOUT}s. Thinking "
846
+ f"models can need longer on a big context — raise it with "
847
+ f"--timeout SECONDS (or POLYCLAUDE_TIMEOUT)."))
848
+ else:
849
+ _log(f"!!! {e!r}")
850
+ sse = _error_sse(model, repr(e))
821
851
  flow.response = http.Response.make(
822
852
  200, sse, {"content-type": "text/event-stream; charset=utf-8"})
823
853
 
@@ -199,6 +199,10 @@ def main(argv=None):
199
199
  help="don't append the '⚠ model tried to STOP' banner")
200
200
  ap.add_argument("--reasoning", choices=["low", "medium", "high"],
201
201
  help="OpenAI reasoning depth (default high)")
202
+ ap.add_argument("--timeout", type=int, default=600, metavar="SECONDS",
203
+ help="how long to wait for the backend to finish a turn "
204
+ "(default 600; raise it for deep-thinking models on a "
205
+ "big context)")
202
206
  ap.add_argument("--hue", type=float, default=110.0,
203
207
  help="accent hue for the branding, degrees (default 110 = green)")
204
208
  ap.add_argument("--resume", nargs="?", const="", default=None, metavar="SESSION",
@@ -276,6 +280,7 @@ def main(argv=None):
276
280
  "POLYCLAUDE_SMALL": prov["small"],
277
281
  "POLYCLAUDE_KEY": key or "none",
278
282
  "POLYCLAUDE_REASONING": args.reasoning or "high",
283
+ "POLYCLAUDE_TIMEOUT": str(args.timeout),
279
284
  "POLYCLAUDE_AUTOCONTINUE": "1",
280
285
  "POLYCLAUDE_LOG": log,
281
286
  })
@@ -64,6 +64,19 @@ PROVIDERS = {
64
64
  "key_env": ["DEEPSEEK_API_KEY"],
65
65
  "help": "Get a key at https://platform.deepseek.com/api_keys",
66
66
  },
67
+ "glm": {
68
+ # Z.ai (Zhipu). glm-5.3 is the flagship (1M context, 128K output,
69
+ # always-thinking, text-only). glm-5.3-flash is the cheap tier and takes
70
+ # images, so it carries the side-call slot.
71
+ # GLM Coding Plan subscribers have a SEPARATE key and endpoint:
72
+ # POLYCLAUDE_BASE=https://api.z.ai/api/coding/paas/v4
73
+ "base": "https://api.z.ai/api/paas/v4",
74
+ "model": "glm-5.3",
75
+ "small": "glm-5.3-flash",
76
+ "key_env": ["ZAI_API_KEY", "GLM_API_KEY", "ZHIPUAI_API_KEY"],
77
+ "aliases": ["zai"],
78
+ "help": "Get a key at https://z.ai/manage-apikey/apikey-list",
79
+ },
67
80
  "ollama": {
68
81
  "base": "http://127.0.0.1:11434/v1",
69
82
  "model": "qwen2.5-coder",
File without changes
File without changes
File without changes
File without changes
File without changes