pythinker-code 0.43.0__py3-none-any.whl → 0.45.0__py3-none-any.whl
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.
- pythinker_code/CHANGELOG.md +97 -0
- pythinker_code/agents/default/system.md +1 -12
- pythinker_code/auth/__init__.py +2 -0
- pythinker_code/auth/kimi.py +278 -0
- pythinker_code/auth/moonshot.py +2 -1
- pythinker_code/auth/platforms.py +17 -0
- pythinker_code/auth/z_ai.py +35 -1
- pythinker_code/cli/__init__.py +13 -13
- pythinker_code/cli/_lazy_group.py +12 -2
- pythinker_code/cli/{vis.py → dashboard.py} +4 -4
- pythinker_code/cli/info.py +52 -0
- pythinker_code/cli/system_prompt.py +76 -0
- pythinker_code/config.py +55 -18
- pythinker_code/dashboard/api/__init__.py +5 -0
- pythinker_code/{vis → dashboard}/api/sessions.py +4 -3
- pythinker_code/{vis → dashboard}/api/statistics.py +11 -3
- pythinker_code/{vis → dashboard}/api/system.py +2 -2
- pythinker_code/{vis → dashboard}/app.py +28 -14
- pythinker_code/{vis/static/assets/highlighted-body-OFNGDK62-Bvu9aDxC.js → dashboard/static/assets/highlighted-body-OFNGDK62-BxVUIXn9.js} +1 -1
- pythinker_code/{vis/static/assets/index-Cot2VCM1.css → dashboard/static/assets/index-DAN_VzOP.css} +1 -1
- pythinker_code/{vis/static/assets/index-Bf7NpcRg.js → dashboard/static/assets/index-DYyxzHPY.js} +6 -6
- pythinker_code/dashboard/static/assets/mermaid-GHXKKRXX-BR6KjYwB.js +1 -0
- pythinker_code/{vis → dashboard}/static/index.html +2 -2
- pythinker_code/memory/recall.py +5 -2
- pythinker_code/share.py +10 -2
- pythinker_code/soul/agent.py +117 -2
- pythinker_code/soul/approval.py +89 -7
- pythinker_code/soul/flow_runner.py +3 -0
- pythinker_code/soul/pythinkersoul.py +177 -6
- pythinker_code/soul/slash.py +25 -0
- pythinker_code/soul/toolset.py +37 -13
- pythinker_code/subagents/discovery.py +16 -0
- pythinker_code/subagents/models.py +4 -0
- pythinker_code/tools/agent/__init__.py +52 -0
- pythinker_code/tools/file/__init__.py +14 -5
- pythinker_code/tools/file/read.py +8 -0
- pythinker_code/tools/file/replace.py +35 -0
- pythinker_code/tools/file/write.py +52 -3
- pythinker_code/tools/shell/__init__.py +5 -1
- pythinker_code/tools/shell/bash.md +2 -2
- pythinker_code/tools/shell/powershell.md +2 -2
- pythinker_code/ui/shell/__init__.py +9 -7
- pythinker_code/ui/shell/components/diff.py +6 -6
- pythinker_code/ui/shell/oauth.py +20 -2
- pythinker_code/ui/shell/selectors/settings.py +25 -0
- pythinker_code/ui/shell/slash.py +149 -6
- pythinker_code/ui/shell/stats_pricing.py +5 -1
- pythinker_code/ui/shell/tool_renderers/agent.py +234 -10
- pythinker_code/ui/shell/update.py +20 -94
- pythinker_code/ui/shell/usage_adapters/minimax.py +93 -50
- pythinker_code/ui/shell/visualize/_blocks.py +0 -13
- pythinker_code/ui/shell/visualize/_interactive.py +1 -1
- pythinker_code/ui/shell/visualize/_live_view.py +8 -23
- pythinker_code/ui/theme.py +9 -9
- pythinker_code/update_policy.py +89 -0
- pythinker_code/utils/file_read_cache.py +65 -0
- pythinker_code/utils/path.py +48 -0
- pythinker_code/utils/pyinstaller.py +6 -6
- pythinker_code/utils/server.py +2 -2
- pythinker_code/utils/subprocess_env.py +1 -1
- pythinker_code/web/static/assets/architecture-7EHR7CIX-CxeoRZcL.js +1 -0
- pythinker_code/web/static/assets/{architectureDiagram-3BPJPVTR-BFNDjk0o.js → architectureDiagram-3BPJPVTR-Cmrv4t4A.js} +1 -1
- pythinker_code/web/static/assets/{blockDiagram-GPEHLZMM-DavOtQzm.js → blockDiagram-GPEHLZMM-DJtt-l4b.js} +1 -1
- pythinker_code/web/static/assets/{bootstrap-CiM9CMSL.js → bootstrap-GQDZdCZn.js} +5 -5
- pythinker_code/web/static/assets/{c4Diagram-AAUBKEIU-B5oqQ4Nw.js → c4Diagram-AAUBKEIU-o0KsSeIT.js} +1 -1
- pythinker_code/web/static/assets/channel-CzOfc6qg.js +1 -0
- pythinker_code/web/static/assets/{chunk-2J33WTMH-DUf3kMx5.js → chunk-2J33WTMH-CVPDc3uI.js} +1 -1
- pythinker_code/web/static/assets/{chunk-3OPIFGDE-BqWQTT-T.js → chunk-3OPIFGDE-Tu9TJ8FO.js} +1 -1
- pythinker_code/web/static/assets/{chunk-5ZQYHXKU-B_tCceNT.js → chunk-5ZQYHXKU-CoBmhCur.js} +1 -1
- pythinker_code/web/static/assets/{chunk-727SXJPM-BTlgorYU.js → chunk-727SXJPM-DByzCodm.js} +1 -1
- pythinker_code/web/static/assets/{chunk-AQP2D5EJ-DGXLLDXi.js → chunk-AQP2D5EJ-DclqBNWp.js} +1 -1
- pythinker_code/web/static/assets/{chunk-CSCIHK7Q-DWI_ps1p.js → chunk-CSCIHK7Q-CUIX1-rb.js} +1 -1
- pythinker_code/web/static/assets/{chunk-JAPRZBRM-CNbI-ZM-.js → chunk-JAPRZBRM-gOR8tSRc.js} +4 -4
- pythinker_code/web/static/assets/{chunk-KSCS5N6A-BesuoyNL.js → chunk-KSCS5N6A-hta5hym2.js} +1 -1
- pythinker_code/web/static/assets/{chunk-L5ZTLDWV-C1CtDpYH.js → chunk-L5ZTLDWV-cj__FNrg.js} +1 -1
- pythinker_code/web/static/assets/{chunk-LZXEDZCA-BXyvAmrA.js → chunk-LZXEDZCA-CSHDE7c1.js} +2 -2
- pythinker_code/web/static/assets/{chunk-ND2GUHAM-oDsfNjJW.js → chunk-ND2GUHAM-DMFQO6Cb.js} +1 -1
- pythinker_code/web/static/assets/{chunk-NZK2D7GU-BQLz5Zcv.js → chunk-NZK2D7GU-Cjm014we.js} +1 -1
- pythinker_code/web/static/assets/{chunk-O5CBEL6O-dMFdGz_Q.js → chunk-O5CBEL6O-BSr1l1Fd.js} +1 -1
- pythinker_code/web/static/assets/{chunk-WU5MYG2G-DjdJnChl.js → chunk-WU5MYG2G-veNzkohb.js} +1 -1
- pythinker_code/web/static/assets/classDiagram-4FO5ZUOK-Cl__vhhZ.js +1 -0
- pythinker_code/web/static/assets/classDiagram-v2-Q7XG4LA2-Cl__vhhZ.js +1 -0
- pythinker_code/web/static/assets/{code-block-IT6T5CEO-D-9Ul0bM.js → code-block-IT6T5CEO-DTgIHAkQ.js} +1 -1
- pythinker_code/web/static/assets/{dagre-BM42HDAG-DtiMfqCu.js → dagre-BM42HDAG-D-_HCbAC.js} +1 -1
- pythinker_code/web/static/assets/{diagram-2AECGRRQ-DOTI1tbw.js → diagram-2AECGRRQ-CvX58Rpg.js} +1 -1
- pythinker_code/web/static/assets/{diagram-5GNKFQAL-B2usKm-c.js → diagram-5GNKFQAL-CsRaiNkg.js} +1 -1
- pythinker_code/web/static/assets/{diagram-KO2AKTUF-P6LKGh5L.js → diagram-KO2AKTUF-Dn6aiXG5.js} +1 -1
- pythinker_code/web/static/assets/{diagram-LMA3HP47-CUOk9Ref.js → diagram-LMA3HP47-DXQlab3r.js} +1 -1
- pythinker_code/web/static/assets/{diagram-OG6HWLK6-BrwkH_cl.js → diagram-OG6HWLK6-BWB_FmTS.js} +1 -1
- pythinker_code/web/static/assets/{dist-C8_WGaGi.js → dist-C8Q1EY4S.js} +1 -1
- pythinker_code/web/static/assets/{erDiagram-TEJ5UH35-CVGVAjD2.js → erDiagram-TEJ5UH35-BrQiudEc.js} +1 -1
- pythinker_code/web/static/assets/eventmodeling-FCH6USID-1z7xo-Al.js +1 -0
- pythinker_code/web/static/assets/{flowDiagram-I6XJVG4X-9eeyqyGY.js → flowDiagram-I6XJVG4X-C6jARfsw.js} +1 -1
- pythinker_code/web/static/assets/{ganttDiagram-6RSMTGT7-CS6z-Y4q.js → ganttDiagram-6RSMTGT7-DH-i7Wci.js} +1 -1
- pythinker_code/web/static/assets/{gitGraph-WXDBUCRP-CZexM5V4.js → gitGraph-WXDBUCRP-lU8PeAwh.js} +1 -1
- pythinker_code/web/static/assets/{gitGraphDiagram-PVQCEYII-glHNqBti.js → gitGraphDiagram-PVQCEYII-JOoIM40T.js} +1 -1
- pythinker_code/web/static/assets/{index-kiyPWoI5.js → index-BJIE96ev.js} +2 -2
- pythinker_code/web/static/assets/{info-J43DQDTF-BZiJBT7D.js → info-J43DQDTF-B9a8E9HF.js} +1 -1
- pythinker_code/web/static/assets/{infoDiagram-5YYISTIA-BoIHamqU.js → infoDiagram-5YYISTIA-D5wmE10q.js} +1 -1
- pythinker_code/web/static/assets/{ishikawaDiagram-YF4QCWOH-DvG-0Xo4.js → ishikawaDiagram-YF4QCWOH-DzNdTJqt.js} +1 -1
- pythinker_code/web/static/assets/{journeyDiagram-JHISSGLW-CL1FBc-S.js → journeyDiagram-JHISSGLW-CwevEltw.js} +1 -1
- pythinker_code/web/static/assets/{kanban-definition-UN3LZRKU-B60x3RfK.js → kanban-definition-UN3LZRKU-BAmLyJgC.js} +1 -1
- pythinker_code/web/static/assets/{line-CD1jDTNu.js → line-CktbshjS.js} +1 -1
- pythinker_code/web/static/assets/mermaid-VLURNSYL-B9qVFSXg.js +1 -0
- pythinker_code/web/static/assets/{mermaid-parser.core-DtoExEpJ.js → mermaid-parser.core-B1Hc3VHx.js} +2 -2
- pythinker_code/web/static/assets/{mermaid.core-BwwvM-AZ.js → mermaid.core-zqRnwN3B.js} +3 -3
- pythinker_code/web/static/assets/{mindmap-definition-RKZ34NQL-Tz82z8s2.js → mindmap-definition-RKZ34NQL-CamoLUlD.js} +1 -1
- pythinker_code/web/static/assets/{packet-YPE3B663-C9tfQGo2.js → packet-YPE3B663-s78KeeiS.js} +1 -1
- pythinker_code/web/static/assets/{pie-LRSECV5Y-Cuto_YOc.js → pie-LRSECV5Y-DSusaq_V.js} +1 -1
- pythinker_code/web/static/assets/{pieDiagram-4H26LBE5-CmBpYt_m.js → pieDiagram-4H26LBE5-BcJSWsmz.js} +1 -1
- pythinker_code/web/static/assets/{quadrantDiagram-W4KKPZXB-BvgJKjpU.js → quadrantDiagram-W4KKPZXB-Cxp3MYGY.js} +1 -1
- pythinker_code/web/static/assets/{radar-GUYGQ44K-Dgsa_1qu.js → radar-GUYGQ44K-sZyAfli_.js} +1 -1
- pythinker_code/web/static/assets/{requirementDiagram-4Y6WPE33-DBKNIBk3.js → requirementDiagram-4Y6WPE33-DSaeIAHw.js} +1 -1
- pythinker_code/web/static/assets/{sankeyDiagram-5OEKKPKP-BZ7Fq7wY.js → sankeyDiagram-5OEKKPKP-BI87xNZ8.js} +1 -1
- pythinker_code/web/static/assets/{sequenceDiagram-3UESZ5HK-BgyXLY_y.js → sequenceDiagram-3UESZ5HK-B3mOEaPg.js} +1 -1
- pythinker_code/web/static/assets/{stateDiagram-AJRCARHV-uRxhbT5K.js → stateDiagram-AJRCARHV-CloKSCZq.js} +1 -1
- pythinker_code/web/static/assets/stateDiagram-v2-BHNVJYJU-bwtGCtLW.js +1 -0
- pythinker_code/web/static/assets/{timeline-definition-PNZ67QCA-Dc2IbbOT.js → timeline-definition-PNZ67QCA-dy1xxojp.js} +1 -1
- pythinker_code/web/static/assets/{treeView-BLDUP644-DkIy1Jve.js → treeView-BLDUP644-CouMM8Ld.js} +1 -1
- pythinker_code/web/static/assets/{treemap-LRROVOQU-vGlBGkTv.js → treemap-LRROVOQU-BurOGxmB.js} +1 -1
- pythinker_code/web/static/assets/{vennDiagram-CIIHVFJN-D6thfpFu.js → vennDiagram-CIIHVFJN-ceoQfI8-.js} +1 -1
- pythinker_code/web/static/assets/{wardley-L42UT6IY-CwwtrRFS.js → wardley-L42UT6IY-DQIeshOY.js} +1 -1
- pythinker_code/web/static/assets/{wardleyDiagram-YWT4CUSO-VTtMKFDl.js → wardleyDiagram-YWT4CUSO-yJgnjbxz.js} +1 -1
- pythinker_code/web/static/assets/{xychartDiagram-2RQKCTM6-CcGq54eW.js → xychartDiagram-2RQKCTM6-jMkhXWCc.js} +1 -1
- pythinker_code/web/static/index.html +1 -1
- {pythinker_code-0.43.0.dist-info → pythinker_code-0.45.0.dist-info}/METADATA +27 -28
- {pythinker_code-0.43.0.dist-info → pythinker_code-0.45.0.dist-info}/RECORD +139 -135
- pythinker_code/vis/api/__init__.py +0 -5
- pythinker_code/vis/static/assets/mermaid-GHXKKRXX-C-XxnFiA.js +0 -1
- pythinker_code/web/static/assets/architecture-7EHR7CIX-ClfnoHaV.js +0 -1
- pythinker_code/web/static/assets/channel-CzC7Y4LO.js +0 -1
- pythinker_code/web/static/assets/classDiagram-4FO5ZUOK-DfbkecQ0.js +0 -1
- pythinker_code/web/static/assets/classDiagram-v2-Q7XG4LA2-DfbkecQ0.js +0 -1
- pythinker_code/web/static/assets/eventmodeling-FCH6USID-DrRs4guh.js +0 -1
- pythinker_code/web/static/assets/mermaid-VLURNSYL-CTw9ASXc.js +0 -1
- pythinker_code/web/static/assets/stateDiagram-v2-BHNVJYJU-K-uskVhe.js +0 -1
- /pythinker_code/{vis → dashboard}/__init__.py +0 -0
- /pythinker_code/{vis → dashboard}/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- /pythinker_code/{vis → dashboard}/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- /pythinker_code/{vis → dashboard}/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- /pythinker_code/{vis → dashboard}/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- /pythinker_code/{vis → dashboard}/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- /pythinker_code/{vis → dashboard}/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- /pythinker_code/{vis → dashboard}/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- {pythinker_code-0.43.0.dist-info → pythinker_code-0.45.0.dist-info}/WHEEL +0 -0
- {pythinker_code-0.43.0.dist-info → pythinker_code-0.45.0.dist-info}/entry_points.txt +0 -0
- {pythinker_code-0.43.0.dist-info → pythinker_code-0.45.0.dist-info}/licenses/LICENSE +0 -0
- {pythinker_code-0.43.0.dist-info → pythinker_code-0.45.0.dist-info}/licenses/NOTICE +0 -0
pythinker_code/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,103 @@ GitHub Releases page; `0.8.0` is the new starting line.
|
|
|
15
15
|
|
|
16
16
|
## Unreleased
|
|
17
17
|
|
|
18
|
+
## 0.45.0 (2026-06-14)
|
|
19
|
+
|
|
20
|
+
- **Agent-tracing dashboard.** Added `pythinker dashboard` — a local web UI for inspecting sessions, wire events, context messages, tool statistics, and usage over time. It is also reachable from the interactive shell via the `/reports` slash command (aliased `/dashboard`).
|
|
21
|
+
- **Gemini finish_reason is now sticky once truncation is detected.** A second candidate with any non-`MAX_TOKENS` finish reason no longer overwrites a previously captured `"length"` signal at either the streaming or non-streaming path in the Google GenAI provider.
|
|
22
|
+
- **Budget-exhausted and stuck-loop messages are now visible in the shell.** Both handoff messages were appended to context but never sent to the wire, so the interactive shell showed no feedback when a spend ceiling or stuck-loop exit fired. Both now emit a `TextPart` wire event so the message appears in the shell.
|
|
23
|
+
- **Dark-theme prompt_toolkit border colors now match the TUI token constants.** Six stale slate hex values in `_PROMPT_STYLE_DARK` diverged from the current `border` and `border_muted` token values; they are now in sync and a parity test binds them going forward.
|
|
24
|
+
- **Non-review successful agents now show a summary preview in the agent tree.** Completed subagents with a `summary_preview` now display a dim preview line in the RunAgents tree renderer (review runs continue to use the findings table instead).
|
|
25
|
+
- **Non-string values in `required_mcp_servers` YAML are silently dropped.** Integers, booleans, and `null` entries were previously coerced to strings (`"1"`, `"False"`, `"None"`), creating permanently unsatisfiable MCP server names. Only actual string entries are now retained.
|
|
26
|
+
- **Fixed MiniMax token-plan `/usage` accuracy.** The token-plan response now meters by
|
|
27
|
+
percentage (the count fields are 0) and reports reset times in milliseconds; the adapter was
|
|
28
|
+
reading the zero counts (showing "0 requests used") and treating milliseconds as seconds
|
|
29
|
+
(showing resets like "171d" for a 5-hour window). It now reads `*_remaining_percent` and
|
|
30
|
+
converts reset times correctly, so e.g. a week at 82% remaining shows "18% used" resetting in
|
|
31
|
+
hours, not days.
|
|
32
|
+
- **GLM-5.2 (1M context) is now the default Z.AI model.** Logging in with Z.AI selects
|
|
33
|
+
`z-ai/glm-5.2` with its full 1,000,000-token context window. GLM-5.2 is absent from z.ai's
|
|
34
|
+
model-listing API, so it is pinned into the catalog and offered even when discovery omits it;
|
|
35
|
+
if z.ai later lists it, the API definition wins and it is shown once (no duplicate). Earlier
|
|
36
|
+
GLM models (5.1, 5, 5-turbo, 4.7, 4.5-air) remain available.
|
|
37
|
+
- **Kimi K2.7 Code added, and a new Kimi Coding Plan provider.** `kimi-k2.7-code` is now the
|
|
38
|
+
default model on the Moonshot plan, and a separate "Kimi Coding Plan" provider
|
|
39
|
+
(`/login kimi`) targets Moonshot's Anthropic-compatible coding endpoint with `kimi-k2.7-code`.
|
|
40
|
+
- **Verb spinner stays visible while a foreground tool or subagent runs.** The shimmering
|
|
41
|
+
"Working…/Thinking…" activity indicator now persists for the whole active turn — including
|
|
42
|
+
while a foreground tool (such as a shell-started server) or a subagent is executing — instead
|
|
43
|
+
of disappearing until the tool finished. This keeps long tool/subagent waits feeling alive
|
|
44
|
+
rather than frozen. An in-progress todo still swaps the verb for the todo title as before.
|
|
45
|
+
- **Project instructions delivered as a separate authoritative message.** The merged
|
|
46
|
+
`AGENTS.md` is no longer baked into the system prompt; it is delivered as a session-start,
|
|
47
|
+
user-role `<system-reminder>` preamble, assembled fresh on every request from session
|
|
48
|
+
state. This keeps the project rules immune to two regressions a system-prompt move would
|
|
49
|
+
otherwise risk — context compaction can no longer summarize them away (they never enter the
|
|
50
|
+
persisted history) and the dynamic-injection token budget can no longer truncate them —
|
|
51
|
+
while keeping the system prompt free of per-project content. `pythinker system-prompt` shows
|
|
52
|
+
the reminder below a labeled divider so the dump stays faithful.
|
|
53
|
+
- **Inspect the assembled system prompt.** New `pythinker system-prompt` command
|
|
54
|
+
renders and prints the fully-assembled system prompt for an agent
|
|
55
|
+
(`--agent <name>`, `--agent-file <path>`, `--work-dir <dir>`) — substituting the
|
|
56
|
+
live work directory, OS/shell, merged `AGENTS.md`, and discovered skills. It is
|
|
57
|
+
read-only: no session is created, no provider auth is required, and no MCP
|
|
58
|
+
servers are loaded.
|
|
59
|
+
- **Bound parallel tool fan-out.** Parallel-safe tool calls in one turn still
|
|
60
|
+
overlap, but now up to a fixed concurrency cap (10) instead of without bound, so
|
|
61
|
+
a turn that fans out many readers (e.g. dozens of web fetches) can no longer open
|
|
62
|
+
an unbounded number of sockets/file handles at once. Mutating-tool ordering and
|
|
63
|
+
writer exclusivity are unchanged.
|
|
64
|
+
- **Optional per-session spend ceiling.** New `loop_control.max_session_cost_usd`
|
|
65
|
+
config option (off by default). When set, a turn stops with a clear
|
|
66
|
+
budget-exhausted handoff message once the session's accumulated estimated cost
|
|
67
|
+
reaches the ceiling, instead of running to the step limit — and goal
|
|
68
|
+
auto-continuations and agent flows halt too. Best-effort: cost is `0` for models
|
|
69
|
+
with unknown pricing, so the ceiling never blocks when spend cannot be estimated.
|
|
70
|
+
- **Sensitive host files always re-confirm.** Writes to shell startup files
|
|
71
|
+
(`.zshrc`, `.bash_profile`, …), `.git` internals/hooks, the custom `.githooks`
|
|
72
|
+
hooks directory, `.ssh`, `.vscode`, and git
|
|
73
|
+
credentials (`.gitconfig`, `.netrc`, `.git-credentials`) are now classified as a
|
|
74
|
+
distinct edit action that re-confirms every time — even under yolo/auto — and is
|
|
75
|
+
never recorded as session-approved, exactly like edits to pythinker's own config.
|
|
76
|
+
This closes an auto-approve gap where a `.git/hooks` write inside the workspace was
|
|
77
|
+
treated as an ordinary edit.
|
|
78
|
+
- **Accept-edits mode.** New `/accept-edits` toggle auto-approves reversible
|
|
79
|
+
in-workspace ordinary file edits while still prompting for shell, destructive,
|
|
80
|
+
outside-workspace, config-surface, and sensitive host-file edits. It is
|
|
81
|
+
session-local (not persisted) and suppressed by safe mode. Pairs with the deny-set
|
|
82
|
+
above so a `.git/hooks` or shell-rc write is never swept into the auto-approve tier.
|
|
83
|
+
- **Agents can declare required MCP servers.** A markdown agent's frontmatter may set
|
|
84
|
+
`required_mcp_servers: [..]`; spawning that agent (via `Agent` or `RunAgents`) is
|
|
85
|
+
rejected with a clear message when those servers are configured-and-absent, instead of
|
|
86
|
+
wasting a turn on an agent that cannot reach its tools. While MCP is still loading the
|
|
87
|
+
spawn is allowed (the servers may yet connect).
|
|
88
|
+
- **UserPromptSubmit hooks can add context.** A non-blocking `UserPromptSubmit` hook's
|
|
89
|
+
`additionalContext` is now injected into the user turn as a system reminder, so the
|
|
90
|
+
model sees it as context for the prompt (previously only a hook *block* was honored).
|
|
91
|
+
Slash-command parsing still reads only the user's text, never the appended context.
|
|
92
|
+
- **Stale-overwrite guard for file edits.** If you read a file and it then changes on
|
|
93
|
+
disk (edited by you in another window or by another tool), overwriting it with WriteFile
|
|
94
|
+
or editing it with StrReplaceFile is now rejected with "File has been modified since you
|
|
95
|
+
last read it" so external changes are not silently clobbered — read it again first. This
|
|
96
|
+
catches external edits that StrReplaceFile's exact-string matching alone would miss.
|
|
97
|
+
First-contact writes (a file you never read) are unaffected, and a tool's own write
|
|
98
|
+
refreshes the read-state so consecutive edits are never falsely flagged.
|
|
99
|
+
- **Recover from output-token truncation.** When a response is cut off by the
|
|
100
|
+
output-token limit and makes no tool call, the turn no longer ends with a half-finished
|
|
101
|
+
answer treated as complete — the model is nudged to continue from where it stopped, up
|
|
102
|
+
to `loop_control.max_truncation_recoveries` times per turn (default 3; `0` disables).
|
|
103
|
+
pythinker-core now surfaces the provider's truncation signal so the loop can detect it.
|
|
104
|
+
|
|
105
|
+
Upgrade with `pythinker update`, `pip install --upgrade pythinker-code==0.45.0`, or use the native installer for your platform from the [Releases page](https://github.com/Pythoughts-labs/pythinker-code/releases/latest).
|
|
106
|
+
|
|
107
|
+
## 0.44.0 (2026-06-13)
|
|
108
|
+
|
|
109
|
+
- **Toggle auto-update from the CLI.** Running `/update` now opens a menu — *Check for updates now* (the default, so a bare `/update` + Enter still checks immediately) or *Auto-update on startup* with its current state — so the toggle is discoverable without knowing a subcommand. `/update auto on|off` still sets it directly, and `/update auto` with no value opens an interactive On/Off picker (cursor defaulted to the current setting). The same toggle appears in the interactive `/settings` panel, and `pythinker info` reports the auto-update status. All surfaces show the *effective* state — an external override (`PYTHINKER_CLI_NO_AUTO_UPDATE` or a source checkout) is surfaced as the reason, renders the `/settings` row read-only, and makes `/update auto` report the read-only state rather than popping a no-op picker, so the toggle is never a silent no-op.
|
|
110
|
+
- **Windows in-app update no longer shows a spurious "could not close the program" error.** The native installer now waits for the launching `pythinker.exe` to fully exit (its PID is passed via `/PID`) before its Restart Manager scan runs, so the scan no longer races the launcher's teardown into a false "close the program and retry" dialog. The update already succeeded in that case; now it completes cleanly without the alarming prompt.
|
|
111
|
+
- **Simplified the Windows pip/uv/pipx update path.** Now that every shipped Windows install updates through the native installer, the Windows-only detached-spawn upgrade helper is removed; the remaining pip/uv/pipx path (a Windows source checkout, or any macOS/Linux install) runs the upgrade inline like POSIX, surfacing real command output and errors instead of a fire-and-forget process.
|
|
112
|
+
|
|
113
|
+
Upgrade with `pythinker update`, `pip install --upgrade pythinker-code==0.44.0`, or use the native installer for your platform from the [Releases page](https://github.com/Pythoughts-labs/pythinker-code/releases/latest).
|
|
114
|
+
|
|
18
115
|
## 0.43.0 (2026-06-13)
|
|
19
116
|
|
|
20
117
|
- **Silent startup auto-updates (default on).** Managed and native installs now check for and apply updates in the background at startup, surfacing a restart-to-apply notice instead of a blocking prompt. Opt out with `auto_update = false` in config or `PYTHINKER_AUTO_UPDATE=0` in the environment. The Windows update path that replaces the running binary no longer escapes as an uncaught `SystemExit` and crashes the shell.
|
|
@@ -242,19 +242,8 @@ ${PYTHINKER_ADDITIONAL_DIRS_INFO}
|
|
|
242
242
|
## 11. Project Instructions (AGENTS.md)
|
|
243
243
|
|
|
244
244
|
`AGENTS.md` files carry the agent-facing context a README omits — build steps, test commands, conventions, structure, and user preferences — kept separate so agents have a predictable place for instructions while READMEs stay human-focused.
|
|
245
|
-
{% if PYTHINKER_AGENTS_MD %}
|
|
246
245
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
${PYTHINKER_AGENTS_MD_FENCE}
|
|
250
|
-
${PYTHINKER_AGENTS_MD}
|
|
251
|
-
${PYTHINKER_AGENTS_MD_FENCE}
|
|
252
|
-
|
|
253
|
-
Treat the merged block as complete for the root-to-working-directory range; look for additional `AGENTS.md` only in directories **below** the working directory and apply them by the same precedence when editing there.
|
|
254
|
-
{% else %}
|
|
255
|
-
|
|
256
|
-
No `AGENTS.md` files were found between the project root and the working directory; look for them only in directories **below** the working directory and apply them when editing there.
|
|
257
|
-
{% endif %}
|
|
246
|
+
When any `AGENTS.md` files apply between the project root and the working directory, their merged content is **delivered as a separate authoritative message at the start of this session** — every file from the project root down to the working directory, deeper (more specific) files overriding shallower ones, each governing its own directory and everything beneath it. Treat that merged message as complete for the root-to-working-directory range, with the same authority as these instructions; look for additional `AGENTS.md` only in directories **below the working directory** and apply them by the same precedence when editing there.
|
|
258
247
|
|
|
259
248
|
Precedence per §2. `README`/`README.md` files are optional supplementary context, not instructions. If a change you make invalidates anything an `AGENTS.md` documents (build/test commands, conventions, structure, workflows), update that `AGENTS.md` in the same change so it stays trustworthy.
|
|
260
249
|
|
pythinker_code/auth/__init__.py
CHANGED
|
@@ -7,6 +7,7 @@ OPENAI_CHATGPT_PLATFORM_ID = "openai-chatgpt"
|
|
|
7
7
|
OPENCODE_GO_PLATFORM_ID = "opencode-go"
|
|
8
8
|
MINIMAX_PLATFORM_ID = "minimax"
|
|
9
9
|
MOONSHOT_PLATFORM_ID = "moonshot"
|
|
10
|
+
KIMI_PLATFORM_ID = "kimi"
|
|
10
11
|
DEEPSEEK_PLATFORM_ID = "deepseek"
|
|
11
12
|
ANTHROPIC_PLATFORM_ID = "anthropic"
|
|
12
13
|
OPENROUTER_PLATFORM_ID = "openrouter"
|
|
@@ -18,6 +19,7 @@ __all__ = [
|
|
|
18
19
|
"ALIBABA_PLATFORM_ID",
|
|
19
20
|
"ANTHROPIC_PLATFORM_ID",
|
|
20
21
|
"DEEPSEEK_PLATFORM_ID",
|
|
22
|
+
"KIMI_PLATFORM_ID",
|
|
21
23
|
"LM_STUDIO_PLATFORM_ID",
|
|
22
24
|
"MINIMAX_PLATFORM_ID",
|
|
23
25
|
"MOONSHOT_PLATFORM_ID",
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import AsyncIterator, Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
import aiohttp
|
|
9
|
+
from pydantic import SecretStr
|
|
10
|
+
|
|
11
|
+
from pythinker_code.auth import KIMI_PLATFORM_ID
|
|
12
|
+
from pythinker_code.auth.oauth import OAuthEvent
|
|
13
|
+
from pythinker_code.auth.platforms import managed_model_key, managed_provider_key
|
|
14
|
+
from pythinker_code.config import Config, LLMModel, LLMProvider, save_config
|
|
15
|
+
from pythinker_code.thinking import apply_login_thinking_defaults
|
|
16
|
+
from pythinker_code.utils.aiohttp import new_client_session
|
|
17
|
+
|
|
18
|
+
# The Kimi coding plan is served from Moonshot's Anthropic-compatible endpoint
|
|
19
|
+
# (ANTHROPIC_BASE_URL in the Claude Code integration guide) and authenticates
|
|
20
|
+
# with the same Moonshot API key. It is a distinct plan from the OpenAI-compatible
|
|
21
|
+
# Moonshot provider (`auth/moonshot.py`), which targets `api.moonshot.ai/v1`.
|
|
22
|
+
KIMI_BASE_URL = "https://api.moonshot.ai/anthropic"
|
|
23
|
+
KIMI_MODELS_URL = "https://api.moonshot.ai/anthropic/v1/models"
|
|
24
|
+
KIMI_PROVIDER_KEY = managed_provider_key(KIMI_PLATFORM_ID)
|
|
25
|
+
KIMI_DEFAULT_MODEL_ALIAS = managed_model_key(KIMI_PLATFORM_ID, "kimi-k2.7-code")
|
|
26
|
+
KIMI_MODEL_DISCOVERY_TIMEOUT = aiohttp.ClientTimeout(total=15, sock_connect=8, sock_read=10)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class KimiModel:
|
|
31
|
+
model_id: str
|
|
32
|
+
alias_suffix: str
|
|
33
|
+
display_name: str
|
|
34
|
+
provider_key: str = KIMI_PROVIDER_KEY
|
|
35
|
+
max_context_size: int = 262_144
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def alias(self) -> str:
|
|
39
|
+
return f"{KIMI_PLATFORM_ID}/{self.alias_suffix}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
KIMI_MODELS: tuple[KimiModel, ...] = (
|
|
43
|
+
KimiModel("kimi-k2.7-code", "kimi-k2.7-code", "Kimi K2.7 Code"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_kimi_api_key_from_env() -> str | None:
|
|
48
|
+
# The coding plan reuses the Moonshot API key; accept either name.
|
|
49
|
+
for var in ("KIMI_API_KEY", "MOONSHOT_API_KEY"):
|
|
50
|
+
value = os.getenv(var)
|
|
51
|
+
if value and value.strip():
|
|
52
|
+
return value.strip()
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _model_by_id() -> dict[str, KimiModel]:
|
|
57
|
+
return {model.model_id: model for model in KIMI_MODELS}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_discovered_models(data: object) -> tuple[KimiModel, ...] | None:
|
|
61
|
+
"""Return parsed models, or None if the payload is structurally invalid.
|
|
62
|
+
|
|
63
|
+
Only models already in the curated catalog are kept: the coding plan is
|
|
64
|
+
focused on `kimi-k2.7-code`, and discovery is used to refresh its context
|
|
65
|
+
window/display name, not to widen the plan.
|
|
66
|
+
"""
|
|
67
|
+
if not isinstance(data, dict):
|
|
68
|
+
return None
|
|
69
|
+
raw_items = cast(dict[str, Any], data).get("data")
|
|
70
|
+
if not isinstance(raw_items, list):
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
catalog = _model_by_id()
|
|
74
|
+
seen: set[str] = set()
|
|
75
|
+
result: list[KimiModel] = []
|
|
76
|
+
for raw_item in cast(list[Any], raw_items):
|
|
77
|
+
if not isinstance(raw_item, Mapping):
|
|
78
|
+
continue
|
|
79
|
+
item = cast(Mapping[str, Any], raw_item)
|
|
80
|
+
model_id = item.get("id")
|
|
81
|
+
if not isinstance(model_id, str) or model_id not in catalog or model_id in seen:
|
|
82
|
+
continue
|
|
83
|
+
seen.add(model_id)
|
|
84
|
+
base = catalog[model_id]
|
|
85
|
+
max_ctx = base.max_context_size
|
|
86
|
+
ctx = item.get("context_length")
|
|
87
|
+
if isinstance(ctx, int) and ctx > 0:
|
|
88
|
+
max_ctx = ctx
|
|
89
|
+
display_name = base.display_name
|
|
90
|
+
api_name = item.get("display_name")
|
|
91
|
+
if isinstance(api_name, str) and api_name.strip():
|
|
92
|
+
display_name = api_name.strip()
|
|
93
|
+
result.append(
|
|
94
|
+
KimiModel(
|
|
95
|
+
model_id=base.model_id,
|
|
96
|
+
alias_suffix=base.alias_suffix,
|
|
97
|
+
display_name=display_name,
|
|
98
|
+
provider_key=base.provider_key,
|
|
99
|
+
max_context_size=max_ctx,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
return tuple(result)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def _discover_kimi_models(api_key: str) -> tuple[KimiModel, ...] | None:
|
|
106
|
+
async with (
|
|
107
|
+
new_client_session(timeout=KIMI_MODEL_DISCOVERY_TIMEOUT) as session,
|
|
108
|
+
session.get(
|
|
109
|
+
KIMI_MODELS_URL,
|
|
110
|
+
headers={"Authorization": f"Bearer {api_key}", "x-api-key": api_key},
|
|
111
|
+
raise_for_status=True,
|
|
112
|
+
) as response,
|
|
113
|
+
):
|
|
114
|
+
payload = await response.json(content_type=None)
|
|
115
|
+
return _parse_discovered_models(payload)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _apply_kimi_config(
|
|
119
|
+
config: Config,
|
|
120
|
+
api_key: SecretStr,
|
|
121
|
+
models: tuple[KimiModel, ...] = KIMI_MODELS,
|
|
122
|
+
) -> None:
|
|
123
|
+
config.providers[KIMI_PROVIDER_KEY] = LLMProvider(
|
|
124
|
+
type="anthropic",
|
|
125
|
+
base_url=KIMI_BASE_URL,
|
|
126
|
+
api_key=api_key,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
provider_keys = {KIMI_PROVIDER_KEY}
|
|
130
|
+
for key, model in list(config.models.items()):
|
|
131
|
+
if model.provider in provider_keys:
|
|
132
|
+
del config.models[key]
|
|
133
|
+
|
|
134
|
+
for model in models:
|
|
135
|
+
config.models[model.alias] = LLMModel(
|
|
136
|
+
provider=model.provider_key,
|
|
137
|
+
model=model.model_id,
|
|
138
|
+
max_context_size=model.max_context_size,
|
|
139
|
+
display_name=model.display_name,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
fallback = next(
|
|
143
|
+
(m.alias for m in models),
|
|
144
|
+
next(iter(config.models), ""),
|
|
145
|
+
)
|
|
146
|
+
if KIMI_DEFAULT_MODEL_ALIAS in config.models:
|
|
147
|
+
config.default_model = KIMI_DEFAULT_MODEL_ALIAS
|
|
148
|
+
else:
|
|
149
|
+
config.default_model = fallback
|
|
150
|
+
apply_login_thinking_defaults(config, thinking=False, effort="off")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def login_kimi_api_key(
|
|
154
|
+
config: Config, api_key: str | None = None
|
|
155
|
+
) -> AsyncIterator[OAuthEvent]:
|
|
156
|
+
if not config.is_from_default_location:
|
|
157
|
+
yield OAuthEvent(
|
|
158
|
+
"error",
|
|
159
|
+
"Login requires the default config file; restart without --config/--config-file.",
|
|
160
|
+
)
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
resolved_key = (api_key or get_kimi_api_key_from_env() or "").strip()
|
|
164
|
+
if not resolved_key:
|
|
165
|
+
yield OAuthEvent("error", "Kimi API key is required.")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
models = KIMI_MODELS
|
|
169
|
+
try:
|
|
170
|
+
discovered = await _discover_kimi_models(resolved_key)
|
|
171
|
+
if discovered is not None and discovered:
|
|
172
|
+
models = discovered
|
|
173
|
+
except aiohttp.ClientResponseError as exc:
|
|
174
|
+
if exc.status in {401, 403}:
|
|
175
|
+
yield OAuthEvent("error", "Invalid Kimi API key; the key was not saved.")
|
|
176
|
+
return
|
|
177
|
+
yield OAuthEvent(
|
|
178
|
+
"info",
|
|
179
|
+
"Kimi model listing is unavailable; using the built-in model list.",
|
|
180
|
+
)
|
|
181
|
+
except (aiohttp.ClientError, TimeoutError, ValueError):
|
|
182
|
+
yield OAuthEvent(
|
|
183
|
+
"info",
|
|
184
|
+
"Kimi model listing is unavailable; using the built-in model list.",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
_apply_kimi_config(config, SecretStr(resolved_key), models=models)
|
|
188
|
+
save_config(config)
|
|
189
|
+
yield OAuthEvent("success", f"Kimi configured with model {config.default_model}.")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def logout_kimi(config: Config) -> AsyncIterator[OAuthEvent]:
|
|
193
|
+
if not config.is_from_default_location:
|
|
194
|
+
yield OAuthEvent(
|
|
195
|
+
"error",
|
|
196
|
+
"Logout requires the default config file; restart without --config/--config-file.",
|
|
197
|
+
)
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
provider_keys = {KIMI_PROVIDER_KEY}
|
|
201
|
+
config.providers.pop(KIMI_PROVIDER_KEY, None)
|
|
202
|
+
for key, model in list(config.models.items()):
|
|
203
|
+
if model.provider in provider_keys:
|
|
204
|
+
del config.models[key]
|
|
205
|
+
|
|
206
|
+
if config.default_model not in config.models:
|
|
207
|
+
config.default_model = next(iter(config.models), "")
|
|
208
|
+
save_config(config)
|
|
209
|
+
yield OAuthEvent("success", "Logged out of Kimi successfully.")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def apply_kimi_models(config: Config, models: tuple[KimiModel, ...]) -> bool:
|
|
213
|
+
"""Upsert the live Kimi catalog and prune models no longer returned.
|
|
214
|
+
|
|
215
|
+
Preserves user preferences unless the selected Kimi model disappeared.
|
|
216
|
+
"""
|
|
217
|
+
changed = False
|
|
218
|
+
aliases: list[str] = []
|
|
219
|
+
for model in models:
|
|
220
|
+
alias = model.alias
|
|
221
|
+
aliases.append(alias)
|
|
222
|
+
existing = config.models.get(alias)
|
|
223
|
+
if existing is None:
|
|
224
|
+
config.models[alias] = LLMModel(
|
|
225
|
+
provider=model.provider_key,
|
|
226
|
+
model=model.model_id,
|
|
227
|
+
max_context_size=model.max_context_size,
|
|
228
|
+
display_name=model.display_name,
|
|
229
|
+
)
|
|
230
|
+
changed = True
|
|
231
|
+
continue
|
|
232
|
+
if existing.provider != model.provider_key:
|
|
233
|
+
existing.provider = model.provider_key
|
|
234
|
+
changed = True
|
|
235
|
+
if existing.model != model.model_id:
|
|
236
|
+
existing.model = model.model_id
|
|
237
|
+
changed = True
|
|
238
|
+
if existing.max_context_size != model.max_context_size:
|
|
239
|
+
existing.max_context_size = model.max_context_size
|
|
240
|
+
changed = True
|
|
241
|
+
if existing.display_name != model.display_name:
|
|
242
|
+
existing.display_name = model.display_name
|
|
243
|
+
changed = True
|
|
244
|
+
|
|
245
|
+
alias_set = set(aliases)
|
|
246
|
+
removed_default = False
|
|
247
|
+
for alias, model_cfg in list(config.models.items()):
|
|
248
|
+
if model_cfg.provider != KIMI_PROVIDER_KEY:
|
|
249
|
+
continue
|
|
250
|
+
if alias in alias_set:
|
|
251
|
+
continue
|
|
252
|
+
del config.models[alias]
|
|
253
|
+
if config.default_model == alias:
|
|
254
|
+
removed_default = True
|
|
255
|
+
changed = True
|
|
256
|
+
|
|
257
|
+
if removed_default:
|
|
258
|
+
config.default_model = aliases[0] if aliases else next(iter(config.models), "")
|
|
259
|
+
changed = True
|
|
260
|
+
elif config.default_model and config.default_model not in config.models:
|
|
261
|
+
config.default_model = next(iter(config.models), "")
|
|
262
|
+
changed = True
|
|
263
|
+
return changed
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _kimi_api_key(config: Config) -> str | None:
|
|
267
|
+
provider = config.providers.get(KIMI_PROVIDER_KEY)
|
|
268
|
+
if provider is None:
|
|
269
|
+
return None
|
|
270
|
+
value = provider.api_key.get_secret_value().strip()
|
|
271
|
+
return value or None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
async def refresh_kimi_models(config: Config) -> tuple[KimiModel, ...] | None:
|
|
275
|
+
api_key = _kimi_api_key(config)
|
|
276
|
+
if api_key is None:
|
|
277
|
+
return None
|
|
278
|
+
return await _discover_kimi_models(api_key)
|
pythinker_code/auth/moonshot.py
CHANGED
|
@@ -17,7 +17,7 @@ from pythinker_code.utils.aiohttp import new_client_session
|
|
|
17
17
|
|
|
18
18
|
MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1"
|
|
19
19
|
MOONSHOT_PROVIDER_KEY = managed_provider_key(MOONSHOT_PLATFORM_ID)
|
|
20
|
-
MOONSHOT_DEFAULT_MODEL_ALIAS = managed_model_key(MOONSHOT_PLATFORM_ID, "kimi-k2.
|
|
20
|
+
MOONSHOT_DEFAULT_MODEL_ALIAS = managed_model_key(MOONSHOT_PLATFORM_ID, "kimi-k2.7-code")
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@dataclass(frozen=True, slots=True)
|
|
@@ -34,6 +34,7 @@ class MoonshotModel:
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
MOONSHOT_MODELS: tuple[MoonshotModel, ...] = (
|
|
37
|
+
MoonshotModel("kimi-k2.7-code", "kimi-k2.7-code", "Kimi K2.7 Code"),
|
|
37
38
|
MoonshotModel("kimi-k2.6", "kimi-k2.6", "Kimi K2.6"),
|
|
38
39
|
MoonshotModel("kimi-k2.5", "kimi-k2.5", "Kimi K2.5"),
|
|
39
40
|
MoonshotModel("kimi-k2-thinking", "kimi-k2-thinking", "Kimi K2 Thinking"),
|
pythinker_code/auth/platforms.py
CHANGED
|
@@ -231,6 +231,12 @@ async def refresh_managed_models(config: Config) -> bool:
|
|
|
231
231
|
if not config.is_from_default_location:
|
|
232
232
|
return False
|
|
233
233
|
|
|
234
|
+
from pythinker_code.auth.kimi import (
|
|
235
|
+
KIMI_PROVIDER_KEY,
|
|
236
|
+
KimiModel,
|
|
237
|
+
apply_kimi_models,
|
|
238
|
+
refresh_kimi_models,
|
|
239
|
+
)
|
|
234
240
|
from pythinker_code.auth.minimax import (
|
|
235
241
|
MINIMAX_ANTHROPIC_PROVIDER_KEY,
|
|
236
242
|
MiniMaxModel,
|
|
@@ -267,6 +273,7 @@ async def refresh_managed_models(config: Config) -> bool:
|
|
|
267
273
|
if provider_key in OPENCODE_GO_PROVIDER_KEYS or provider_key in (
|
|
268
274
|
MINIMAX_ANTHROPIC_PROVIDER_KEY,
|
|
269
275
|
ZAI_PROVIDER_KEY,
|
|
276
|
+
KIMI_PROVIDER_KEY,
|
|
270
277
|
):
|
|
271
278
|
continue
|
|
272
279
|
platform_id = parse_managed_provider_key(provider_key)
|
|
@@ -431,6 +438,14 @@ async def refresh_managed_models(config: Config) -> bool:
|
|
|
431
438
|
if z_ai_models is not None and apply_z_ai_models(config, z_ai_models):
|
|
432
439
|
changed = True
|
|
433
440
|
|
|
441
|
+
kimi_models: tuple[KimiModel, ...] | None = None
|
|
442
|
+
try:
|
|
443
|
+
kimi_models = await refresh_kimi_models(config)
|
|
444
|
+
except (aiohttp.ClientError, TimeoutError, ValueError) as exc:
|
|
445
|
+
logger.warning("Failed to refresh Kimi models: {error}", error=exc)
|
|
446
|
+
if kimi_models is not None and apply_kimi_models(config, kimi_models):
|
|
447
|
+
changed = True
|
|
448
|
+
|
|
434
449
|
if changed:
|
|
435
450
|
config_for_save = load_config()
|
|
436
451
|
save_changed = False
|
|
@@ -443,6 +458,8 @@ async def refresh_managed_models(config: Config) -> bool:
|
|
|
443
458
|
save_changed = True
|
|
444
459
|
if z_ai_models is not None and apply_z_ai_models(config_for_save, z_ai_models):
|
|
445
460
|
save_changed = True
|
|
461
|
+
if kimi_models is not None and apply_kimi_models(config_for_save, kimi_models):
|
|
462
|
+
save_changed = True
|
|
446
463
|
if save_changed:
|
|
447
464
|
save_config(config_for_save)
|
|
448
465
|
return changed
|
pythinker_code/auth/z_ai.py
CHANGED
|
@@ -18,7 +18,7 @@ from pythinker_code.utils.aiohttp import new_client_session
|
|
|
18
18
|
ZAI_BASE_URL = "https://api.z.ai/api/anthropic"
|
|
19
19
|
ZAI_MODELS_URL = "https://api.z.ai/api/anthropic/v1/models"
|
|
20
20
|
ZAI_PROVIDER_KEY = managed_provider_key(ZAI_PLATFORM_ID)
|
|
21
|
-
ZAI_DEFAULT_MODEL_ALIAS = managed_model_key(ZAI_PLATFORM_ID, "glm-5.
|
|
21
|
+
ZAI_DEFAULT_MODEL_ALIAS = managed_model_key(ZAI_PLATFORM_ID, "glm-5.2")
|
|
22
22
|
ZAI_MODEL_DISCOVERY_TIMEOUT = aiohttp.ClientTimeout(total=15, sock_connect=8, sock_read=10)
|
|
23
23
|
|
|
24
24
|
|
|
@@ -35,7 +35,18 @@ class ZaiModel:
|
|
|
35
35
|
return f"{ZAI_PLATFORM_ID}/{self.alias_suffix}"
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
# GLM-5.2 is served on z.ai's Anthropic-compatible endpoint under the plain id
|
|
39
|
+
# "glm-5.2", which carries the full 1M-token context window. Verified empirically
|
|
40
|
+
# 2026-06-15 against api.z.ai/api/anthropic: a request with 1,002,378 input
|
|
41
|
+
# tokens succeeded while ~1.05M returned stop_reason="model_context_window_exceeded".
|
|
42
|
+
# The documented "glm-5.2[1m]" suffix is NOT a valid model code here (returns
|
|
43
|
+
# HTTP 400 "Unknown Model") — the plain id already grants 1M, so we use it and
|
|
44
|
+
# set the real window. z.ai's /models listings expose no context field and omit
|
|
45
|
+
# glm-5.2 entirely, so both the id and the size are curated.
|
|
46
|
+
_GLM_5_2 = ZaiModel("glm-5.2", "glm-5.2", "GLM-5.2", max_context_size=1_000_000)
|
|
47
|
+
|
|
38
48
|
ZAI_MODELS: tuple[ZaiModel, ...] = (
|
|
49
|
+
_GLM_5_2,
|
|
39
50
|
ZaiModel("glm-5.1", "glm-5.1", "GLM-5.1", max_context_size=204_800),
|
|
40
51
|
ZaiModel("glm-5", "glm-5", "GLM-5"),
|
|
41
52
|
ZaiModel("glm-5-turbo", "glm-5-turbo", "GLM-5-Turbo"),
|
|
@@ -43,6 +54,26 @@ ZAI_MODELS: tuple[ZaiModel, ...] = (
|
|
|
43
54
|
ZaiModel("glm-4.5-air", "glm-4.5-air", "GLM-4.5-Air", max_context_size=98_304),
|
|
44
55
|
)
|
|
45
56
|
|
|
57
|
+
# Curated models that must always be offered even when z.ai's /models endpoint
|
|
58
|
+
# does not list them. GLM-5.2 is usable for chat but is absent from both the
|
|
59
|
+
# Anthropic and OpenAI-compatible /models listings (verified 2026-06-15), so
|
|
60
|
+
# without pinning it never reaches the model menu and a successful login or
|
|
61
|
+
# periodic refresh would drop it. Discovered entries win for everything else.
|
|
62
|
+
_PINNED_MODELS: tuple[ZaiModel, ...] = (_GLM_5_2,)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _with_pinned_models(models: tuple[ZaiModel, ...]) -> tuple[ZaiModel, ...]:
|
|
66
|
+
"""Prepend curated pinned models the live catalog omitted, deduped by alias.
|
|
67
|
+
|
|
68
|
+
If z.ai later starts returning a pinned model (e.g. it adds "glm-5.2" to its
|
|
69
|
+
/models listing), the discovered entry already occupies that alias, so the
|
|
70
|
+
pin is dropped — the API-provided definition wins and the model appears once,
|
|
71
|
+
never twice. The pin only fills the gap while the endpoint omits it.
|
|
72
|
+
"""
|
|
73
|
+
present = {model.alias for model in models}
|
|
74
|
+
missing = tuple(model for model in _PINNED_MODELS if model.alias not in present)
|
|
75
|
+
return missing + models
|
|
76
|
+
|
|
46
77
|
|
|
47
78
|
def get_z_ai_api_key_from_env() -> str | None:
|
|
48
79
|
value = os.getenv("ZAI_API_KEY")
|
|
@@ -163,6 +194,8 @@ def _apply_z_ai_config(
|
|
|
163
194
|
api_key=api_key,
|
|
164
195
|
)
|
|
165
196
|
|
|
197
|
+
models = _with_pinned_models(models)
|
|
198
|
+
|
|
166
199
|
provider_keys = {ZAI_PROVIDER_KEY}
|
|
167
200
|
for key, model in list(config.models.items()):
|
|
168
201
|
if model.provider in provider_keys:
|
|
@@ -251,6 +284,7 @@ def apply_z_ai_models(config: Config, models: tuple[ZaiModel, ...]) -> bool:
|
|
|
251
284
|
|
|
252
285
|
Preserves user preferences unless the selected Z AI model disappeared.
|
|
253
286
|
"""
|
|
287
|
+
models = _with_pinned_models(models)
|
|
254
288
|
changed = False
|
|
255
289
|
aliases: list[str] = []
|
|
256
290
|
for model in models:
|
pythinker_code/cli/__init__.py
CHANGED
|
@@ -36,11 +36,11 @@ class SwitchToWeb(Exception):
|
|
|
36
36
|
self.session_id = session_id
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
class
|
|
40
|
-
"""Switch to
|
|
39
|
+
class SwitchToDashboard(Exception):
|
|
40
|
+
"""Switch to dashboard (tracing visualizer) interface."""
|
|
41
41
|
|
|
42
|
-
def __init__(self, session_id: str | None = None):
|
|
43
|
-
super().__init__("
|
|
42
|
+
def __init__(self, session_id: str | None = None) -> None:
|
|
43
|
+
super().__init__("switch_to_dashboard")
|
|
44
44
|
self.session_id = session_id
|
|
45
45
|
|
|
46
46
|
|
|
@@ -1042,7 +1042,7 @@ def pythinker(
|
|
|
1042
1042
|
except SwitchToWeb:
|
|
1043
1043
|
preserve_background_tasks = True
|
|
1044
1044
|
raise
|
|
1045
|
-
except
|
|
1045
|
+
except SwitchToDashboard:
|
|
1046
1046
|
preserve_background_tasks = True
|
|
1047
1047
|
raise
|
|
1048
1048
|
finally:
|
|
@@ -1134,10 +1134,10 @@ def pythinker(
|
|
|
1134
1134
|
await asyncio.to_thread(mutate_metadata, _mark_last)
|
|
1135
1135
|
|
|
1136
1136
|
async def _reload_loop(session_id: str | None) -> tuple[str | None, int]:
|
|
1137
|
-
"""Run the main loop, handling Reload/SwitchToWeb/
|
|
1137
|
+
"""Run the main loop, handling Reload/SwitchToWeb/SwitchToDashboard.
|
|
1138
1138
|
|
|
1139
1139
|
Returns:
|
|
1140
|
-
(switch_target, exit_code) where switch_target is "web", "
|
|
1140
|
+
(switch_target, exit_code) where switch_target is "web", "dashboard",
|
|
1141
1141
|
or None if the session ended normally.
|
|
1142
1142
|
"""
|
|
1143
1143
|
last_session: Session | None = None
|
|
@@ -1182,19 +1182,19 @@ def pythinker(
|
|
|
1182
1182
|
if session is not None:
|
|
1183
1183
|
await _post_run(session, ExitCode.SUCCESS)
|
|
1184
1184
|
return "web", ExitCode.SUCCESS
|
|
1185
|
-
except
|
|
1185
|
+
except SwitchToDashboard as e:
|
|
1186
1186
|
if _latest_created_session is not None:
|
|
1187
1187
|
_latest_created_session.release_ownership()
|
|
1188
1188
|
if e.session_id is not None:
|
|
1189
1189
|
session = await Session.find(work_dir, e.session_id)
|
|
1190
1190
|
if session is not None:
|
|
1191
1191
|
await _post_run(session, ExitCode.SUCCESS)
|
|
1192
|
-
return "
|
|
1192
|
+
return "dashboard", ExitCode.SUCCESS
|
|
1193
1193
|
assert last_session is not None
|
|
1194
1194
|
await _post_run(last_session, exit_code)
|
|
1195
1195
|
last_session.release_ownership()
|
|
1196
1196
|
return None, exit_code
|
|
1197
|
-
except (SwitchToWeb,
|
|
1197
|
+
except (SwitchToWeb, SwitchToDashboard):
|
|
1198
1198
|
# Currently handled inside the loop (return), but re-raise explicitly
|
|
1199
1199
|
# so the generic except below never treats them as unexpected errors.
|
|
1200
1200
|
raise
|
|
@@ -1316,7 +1316,7 @@ def pythinker(
|
|
|
1316
1316
|
"Run with --debug for full traceback, or run pythinker export to share diagnostics."
|
|
1317
1317
|
)
|
|
1318
1318
|
raise typer.Exit(code=1) from exc
|
|
1319
|
-
if switch_target in ("web", "
|
|
1319
|
+
if switch_target in ("web", "dashboard"):
|
|
1320
1320
|
from pythinker_code.utils.logging import restore_stderr
|
|
1321
1321
|
|
|
1322
1322
|
restore_stderr()
|
|
@@ -1336,9 +1336,9 @@ def pythinker(
|
|
|
1336
1336
|
|
|
1337
1337
|
run_web_server(open_browser=True)
|
|
1338
1338
|
else:
|
|
1339
|
-
from pythinker_code.
|
|
1339
|
+
from pythinker_code.dashboard.app import run_dashboard_server
|
|
1340
1340
|
|
|
1341
|
-
|
|
1341
|
+
run_dashboard_server(open_browser=True)
|
|
1342
1342
|
elif exit_code != ExitCode.SUCCESS:
|
|
1343
1343
|
raise typer.Exit(code=exit_code)
|
|
1344
1344
|
|