polyclaude 0.1.0__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.
- polyclaude-0.1.0/.github/workflows/ci.yml +25 -0
- polyclaude-0.1.0/.github/workflows/release.yml +23 -0
- polyclaude-0.1.0/.gitignore +29 -0
- polyclaude-0.1.0/LICENSE +21 -0
- polyclaude-0.1.0/PKG-INFO +155 -0
- polyclaude-0.1.0/README.md +134 -0
- polyclaude-0.1.0/docs/prompts.md +47 -0
- polyclaude-0.1.0/docs/providers.md +35 -0
- polyclaude-0.1.0/pyproject.toml +33 -0
- polyclaude-0.1.0/src/polyclaude/__init__.py +3 -0
- polyclaude-0.1.0/src/polyclaude/bridge.py +466 -0
- polyclaude-0.1.0/src/polyclaude/cli.py +206 -0
- polyclaude-0.1.0/src/polyclaude/profiles/concise.md +6 -0
- polyclaude-0.1.0/src/polyclaude/profiles/datascience.md +18 -0
- polyclaude-0.1.0/src/polyclaude/profiles/reviewer.md +14 -0
- polyclaude-0.1.0/src/polyclaude/providers.py +44 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.11", "3.12"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- name: Install
|
|
20
|
+
run: pip install -e .
|
|
21
|
+
- name: Import & CLI smoke test
|
|
22
|
+
run: |
|
|
23
|
+
python -c "import polyclaude, polyclaude.bridge, polyclaude.cli, polyclaude.providers"
|
|
24
|
+
polyclaude --list
|
|
25
|
+
polyclaude --version
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
release:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # required for PyPI Trusted Publishing (OIDC)
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- name: Build sdist and wheel
|
|
19
|
+
run: |
|
|
20
|
+
python -m pip install --upgrade build
|
|
21
|
+
python -m build
|
|
22
|
+
- name: Publish to PyPI
|
|
23
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# secrets — never commit
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
*.key
|
|
5
|
+
*.pem
|
|
6
|
+
|
|
7
|
+
# python
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*.egg-info/
|
|
11
|
+
build/
|
|
12
|
+
dist/
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
.eggs/
|
|
16
|
+
|
|
17
|
+
# tooling
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.ruff_cache/
|
|
21
|
+
|
|
22
|
+
# os / editor
|
|
23
|
+
.DS_Store
|
|
24
|
+
*.swp
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
|
|
28
|
+
# runtime logs
|
|
29
|
+
/tmp/polyclaude*.log
|
polyclaude-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Devesh Pratap
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polyclaude
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Use OpenAI, Gemini, and any OpenAI-compatible model inside the Claude Code CLI.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ibedevesh/polyclaude
|
|
6
|
+
Project-URL: Repository, https://github.com/ibedevesh/polyclaude
|
|
7
|
+
Project-URL: Issues, https://github.com/ibedevesh/polyclaude/issues
|
|
8
|
+
Author-email: Devesh Pratap <mail2deveshpratap@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: anthropic,claude,claude-code,cli,coding-agent,gemini,gpt,llm,openai,proxy
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: mitmproxy>=10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# polyclaude
|
|
23
|
+
|
|
24
|
+
**Use OpenAI, Gemini, or any OpenAI-compatible model inside the Claude Code CLI.**
|
|
25
|
+
|
|
26
|
+
Claude Code is a great terminal coding agent — but it only talks to Anthropic's
|
|
27
|
+
models. `polyclaude` lets you keep the exact Claude Code experience (its UI,
|
|
28
|
+
tools, and agent loop) while the actual model behind it is **GPT, Gemini, a Groq
|
|
29
|
+
model, an OpenRouter model, or a local Ollama model**.
|
|
30
|
+
|
|
31
|
+
It works by running a tiny local proxy that speaks Claude Code's API on one side
|
|
32
|
+
and your provider's API on the other, translating between them on the fly.
|
|
33
|
+
Claude Code itself is never modified.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Claude Code ⇄ Anthropic Messages API ⇄ [ polyclaude ] ⇄ OpenAI / Gemini / …
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pipx install polyclaude # recommended
|
|
45
|
+
# or: uv tool install polyclaude
|
|
46
|
+
# or: pip install polyclaude
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
You'll also need [Claude Code](https://docs.claude.com/claude-code) installed
|
|
50
|
+
(`claude` on your PATH) and an API key for whichever provider you want to use.
|
|
51
|
+
|
|
52
|
+
## Use
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
export GEMINI_API_KEY=... # or OPENAI_API_KEY, GROQ_API_KEY, …
|
|
56
|
+
polyclaude --gemini # Claude Code, powered by Gemini
|
|
57
|
+
polyclaude --openai # …powered by GPT
|
|
58
|
+
polyclaude --openai --model gpt-4.1
|
|
59
|
+
polyclaude --gemini --model gemini-2.5-pro
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That's it — you're dropped into a normal Claude Code session running on the
|
|
63
|
+
model you chose. First run auto-configures the local proxy certificate; nothing
|
|
64
|
+
else to set up.
|
|
65
|
+
|
|
66
|
+
Pass arguments straight through to `claude` after `--`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
polyclaude --gemini -- -p "explain this repo"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Providers
|
|
75
|
+
|
|
76
|
+
| Provider | Flag | Key env | Default model |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| Google Gemini | `--gemini` | `GEMINI_API_KEY` | `gemini-2.5-pro` |
|
|
79
|
+
| OpenAI | `--openai` | `OPENAI_API_KEY` | `gpt-4.1` |
|
|
80
|
+
| Groq | `--groq` | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
|
81
|
+
| OpenRouter | `--openrouter` | `OPENROUTER_API_KEY` | `anthropic/claude-3.5-sonnet` |
|
|
82
|
+
| Ollama (local) | `--ollama` | — | `qwen2.5-coder` |
|
|
83
|
+
|
|
84
|
+
Any model your key can access works via `--model`, including the newest ones
|
|
85
|
+
(Gemini 3.x, GPT-5.x). polyclaude handles the provider-specific details so tool
|
|
86
|
+
calling and reasoning keep working:
|
|
87
|
+
|
|
88
|
+
- **Gemini 3.x** thinking models need their function-call *thought signatures*
|
|
89
|
+
round-tripped across turns — handled automatically.
|
|
90
|
+
- **GPT-5.5 / 5.6** can't use tools and reasoning together on the standard
|
|
91
|
+
endpoint; polyclaude routes them through OpenAI's `/responses` API so you get
|
|
92
|
+
**both** (tune depth with `--reasoning low|medium|high`).
|
|
93
|
+
- Reasoning models reject non-default sampling params — dropped automatically.
|
|
94
|
+
- Turns that hit the output-token cap are transparently continued.
|
|
95
|
+
|
|
96
|
+
Run `polyclaude --list` to see everything.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Specialize the agent
|
|
101
|
+
|
|
102
|
+
Give the agent a persona/use-case with a profile — it's appended to the system
|
|
103
|
+
prompt for the main loop only, so Claude Code's own tooling keeps working:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
polyclaude --gemini --profile datascience
|
|
107
|
+
polyclaude --openai --profile reviewer
|
|
108
|
+
polyclaude --gemini --profile ./my-profile.md # your own file
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Bundled profiles: `datascience`, `reviewer`, `concise`. Or replace the whole
|
|
112
|
+
system prompt with `--system path/to/file.md`.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## How it works
|
|
117
|
+
|
|
118
|
+
`polyclaude` starts [mitmproxy](https://mitmproxy.org) locally with a small
|
|
119
|
+
addon and points Claude Code at it (via `HTTPS_PROXY` + `NODE_EXTRA_CA_CERTS`,
|
|
120
|
+
scoped to that one process). The addon intercepts requests to
|
|
121
|
+
`api.anthropic.com/v1/messages`, translates the Anthropic request into the
|
|
122
|
+
provider's format, calls the provider, and streams the reply back in Anthropic's
|
|
123
|
+
event format. Every other host is passed straight through untouched.
|
|
124
|
+
|
|
125
|
+
Only sessions you launch with `polyclaude` are affected — your normal `claude`
|
|
126
|
+
is unchanged and still uses Anthropic.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## FAQ
|
|
131
|
+
|
|
132
|
+
**Does this change my Claude Code install?** No. It only sets proxy environment
|
|
133
|
+
variables for the session it launches.
|
|
134
|
+
|
|
135
|
+
**Do my keys leave my machine?** Only to the provider you choose (Google,
|
|
136
|
+
OpenAI, etc.), exactly as if you called their API directly. polyclaude has no
|
|
137
|
+
servers.
|
|
138
|
+
|
|
139
|
+
**Quality feels off on some models.** Different models behave differently inside
|
|
140
|
+
Claude Code's harness (its prompt and tools are tuned for Claude). Larger/newer
|
|
141
|
+
models fare better; try `--reasoning high` on OpenAI and compare providers.
|
|
142
|
+
|
|
143
|
+
**Can I use my own endpoint?** Yes — anything OpenAI-compatible. Point at it and
|
|
144
|
+
go (OpenRouter, vLLM, LM Studio, etc.).
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Notes
|
|
149
|
+
|
|
150
|
+
For personal and development use, with your own API keys. Respect the terms of
|
|
151
|
+
service of Claude Code and of whichever model provider you use.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT © Devesh Pratap
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# polyclaude
|
|
2
|
+
|
|
3
|
+
**Use OpenAI, Gemini, or any OpenAI-compatible model inside the Claude Code CLI.**
|
|
4
|
+
|
|
5
|
+
Claude Code is a great terminal coding agent — but it only talks to Anthropic's
|
|
6
|
+
models. `polyclaude` lets you keep the exact Claude Code experience (its UI,
|
|
7
|
+
tools, and agent loop) while the actual model behind it is **GPT, Gemini, a Groq
|
|
8
|
+
model, an OpenRouter model, or a local Ollama model**.
|
|
9
|
+
|
|
10
|
+
It works by running a tiny local proxy that speaks Claude Code's API on one side
|
|
11
|
+
and your provider's API on the other, translating between them on the fly.
|
|
12
|
+
Claude Code itself is never modified.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Claude Code ⇄ Anthropic Messages API ⇄ [ polyclaude ] ⇄ OpenAI / Gemini / …
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pipx install polyclaude # recommended
|
|
24
|
+
# or: uv tool install polyclaude
|
|
25
|
+
# or: pip install polyclaude
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
You'll also need [Claude Code](https://docs.claude.com/claude-code) installed
|
|
29
|
+
(`claude` on your PATH) and an API key for whichever provider you want to use.
|
|
30
|
+
|
|
31
|
+
## Use
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
export GEMINI_API_KEY=... # or OPENAI_API_KEY, GROQ_API_KEY, …
|
|
35
|
+
polyclaude --gemini # Claude Code, powered by Gemini
|
|
36
|
+
polyclaude --openai # …powered by GPT
|
|
37
|
+
polyclaude --openai --model gpt-4.1
|
|
38
|
+
polyclaude --gemini --model gemini-2.5-pro
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's it — you're dropped into a normal Claude Code session running on the
|
|
42
|
+
model you chose. First run auto-configures the local proxy certificate; nothing
|
|
43
|
+
else to set up.
|
|
44
|
+
|
|
45
|
+
Pass arguments straight through to `claude` after `--`:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
polyclaude --gemini -- -p "explain this repo"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Providers
|
|
54
|
+
|
|
55
|
+
| Provider | Flag | Key env | Default model |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| Google Gemini | `--gemini` | `GEMINI_API_KEY` | `gemini-2.5-pro` |
|
|
58
|
+
| OpenAI | `--openai` | `OPENAI_API_KEY` | `gpt-4.1` |
|
|
59
|
+
| Groq | `--groq` | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
|
60
|
+
| OpenRouter | `--openrouter` | `OPENROUTER_API_KEY` | `anthropic/claude-3.5-sonnet` |
|
|
61
|
+
| Ollama (local) | `--ollama` | — | `qwen2.5-coder` |
|
|
62
|
+
|
|
63
|
+
Any model your key can access works via `--model`, including the newest ones
|
|
64
|
+
(Gemini 3.x, GPT-5.x). polyclaude handles the provider-specific details so tool
|
|
65
|
+
calling and reasoning keep working:
|
|
66
|
+
|
|
67
|
+
- **Gemini 3.x** thinking models need their function-call *thought signatures*
|
|
68
|
+
round-tripped across turns — handled automatically.
|
|
69
|
+
- **GPT-5.5 / 5.6** can't use tools and reasoning together on the standard
|
|
70
|
+
endpoint; polyclaude routes them through OpenAI's `/responses` API so you get
|
|
71
|
+
**both** (tune depth with `--reasoning low|medium|high`).
|
|
72
|
+
- Reasoning models reject non-default sampling params — dropped automatically.
|
|
73
|
+
- Turns that hit the output-token cap are transparently continued.
|
|
74
|
+
|
|
75
|
+
Run `polyclaude --list` to see everything.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Specialize the agent
|
|
80
|
+
|
|
81
|
+
Give the agent a persona/use-case with a profile — it's appended to the system
|
|
82
|
+
prompt for the main loop only, so Claude Code's own tooling keeps working:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
polyclaude --gemini --profile datascience
|
|
86
|
+
polyclaude --openai --profile reviewer
|
|
87
|
+
polyclaude --gemini --profile ./my-profile.md # your own file
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Bundled profiles: `datascience`, `reviewer`, `concise`. Or replace the whole
|
|
91
|
+
system prompt with `--system path/to/file.md`.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## How it works
|
|
96
|
+
|
|
97
|
+
`polyclaude` starts [mitmproxy](https://mitmproxy.org) locally with a small
|
|
98
|
+
addon and points Claude Code at it (via `HTTPS_PROXY` + `NODE_EXTRA_CA_CERTS`,
|
|
99
|
+
scoped to that one process). The addon intercepts requests to
|
|
100
|
+
`api.anthropic.com/v1/messages`, translates the Anthropic request into the
|
|
101
|
+
provider's format, calls the provider, and streams the reply back in Anthropic's
|
|
102
|
+
event format. Every other host is passed straight through untouched.
|
|
103
|
+
|
|
104
|
+
Only sessions you launch with `polyclaude` are affected — your normal `claude`
|
|
105
|
+
is unchanged and still uses Anthropic.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## FAQ
|
|
110
|
+
|
|
111
|
+
**Does this change my Claude Code install?** No. It only sets proxy environment
|
|
112
|
+
variables for the session it launches.
|
|
113
|
+
|
|
114
|
+
**Do my keys leave my machine?** Only to the provider you choose (Google,
|
|
115
|
+
OpenAI, etc.), exactly as if you called their API directly. polyclaude has no
|
|
116
|
+
servers.
|
|
117
|
+
|
|
118
|
+
**Quality feels off on some models.** Different models behave differently inside
|
|
119
|
+
Claude Code's harness (its prompt and tools are tuned for Claude). Larger/newer
|
|
120
|
+
models fare better; try `--reasoning high` on OpenAI and compare providers.
|
|
121
|
+
|
|
122
|
+
**Can I use my own endpoint?** Yes — anything OpenAI-compatible. Point at it and
|
|
123
|
+
go (OpenRouter, vLLM, LM Studio, etc.).
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Notes
|
|
128
|
+
|
|
129
|
+
For personal and development use, with your own API keys. Respect the terms of
|
|
130
|
+
service of Claude Code and of whichever model provider you use.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT © Devesh Pratap
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Specializing the system prompt
|
|
2
|
+
|
|
3
|
+
polyclaude can inject a persona / use-case into the agent's system prompt so the
|
|
4
|
+
same Claude Code becomes a specialized assistant — without editing anything in
|
|
5
|
+
Claude Code itself.
|
|
6
|
+
|
|
7
|
+
## Profiles
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
polyclaude --gemini --profile datascience
|
|
11
|
+
polyclaude --openai --profile reviewer
|
|
12
|
+
polyclaude --gemini --profile concise
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The profile text is **appended** to the system prompt of the **main agent
|
|
16
|
+
loop** only. The lightweight side-calls Claude Code makes (conversation titles,
|
|
17
|
+
summaries, etc.) are left alone, so nothing breaks.
|
|
18
|
+
|
|
19
|
+
Bundled profiles: `datascience`, `reviewer`, `concise`.
|
|
20
|
+
|
|
21
|
+
## Your own profile
|
|
22
|
+
|
|
23
|
+
Point `--profile` at any Markdown file:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
polyclaude --gemini --profile ./profiles/backend.md
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
A profile is just plain instructions, e.g.:
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
# Backend engineer
|
|
33
|
+
You work on a Python + FastAPI + Postgres service. Prefer async, type hints,
|
|
34
|
+
and small pure functions. Always add or update tests for changed behavior.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Replace the whole system prompt
|
|
38
|
+
|
|
39
|
+
For full control:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
polyclaude --gemini --system ./my-system.md
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This replaces the main system prompt entirely. Note that Claude Code's built-in
|
|
46
|
+
prompt carries important tool-usage contracts — replacing it wholesale can
|
|
47
|
+
change behavior, so append (a profile) unless you know you want a full swap.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Providers & models
|
|
2
|
+
|
|
3
|
+
polyclaude works with any OpenAI-compatible backend. Presets:
|
|
4
|
+
|
|
5
|
+
| Provider | Flag | Key env | Base URL | Default model |
|
|
6
|
+
|---|---|---|---|---|
|
|
7
|
+
| Gemini | `--gemini` | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | `generativelanguage.googleapis.com/v1beta/openai` | `gemini-2.5-pro` |
|
|
8
|
+
| OpenAI | `--openai` | `OPENAI_API_KEY` | `api.openai.com/v1` | `gpt-4.1` |
|
|
9
|
+
| Groq | `--groq` | `GROQ_API_KEY` | `api.groq.com/openai/v1` | `llama-3.3-70b-versatile` |
|
|
10
|
+
| OpenRouter | `--openrouter` | `OPENROUTER_API_KEY` | `openrouter.ai/api/v1` | `anthropic/claude-3.5-sonnet` |
|
|
11
|
+
| Ollama | `--ollama` | — | `127.0.0.1:11434/v1` | `qwen2.5-coder` |
|
|
12
|
+
|
|
13
|
+
Override the model with `--model NAME`.
|
|
14
|
+
|
|
15
|
+
## Newer models
|
|
16
|
+
|
|
17
|
+
These work; polyclaude smooths over their API differences:
|
|
18
|
+
|
|
19
|
+
- **Gemini 3.x** (`gemini-3.1-pro-preview`, `gemini-pro-latest`, …) — thinking
|
|
20
|
+
models. Their function calls carry a *thought signature* that must be sent
|
|
21
|
+
back on the next turn or the API rejects it; polyclaude stores and replays it.
|
|
22
|
+
- **GPT-5.x / 5.5 / 5.6** — on `/v1/chat/completions` these refuse to use tools
|
|
23
|
+
and reasoning at the same time. polyclaude routes `gpt-5.5`/`gpt-5.6` through
|
|
24
|
+
OpenAI's `/v1/responses` API so you get tools **and** full reasoning; control
|
|
25
|
+
depth with `--reasoning low|medium|high` (default `high`). Reasoning items are
|
|
26
|
+
round-tripped across turns.
|
|
27
|
+
- **o-series / any reasoning model** — non-default `temperature`/`top_p` are
|
|
28
|
+
dropped (they'd be rejected).
|
|
29
|
+
|
|
30
|
+
## Tips
|
|
31
|
+
|
|
32
|
+
- `--reasoning` only affects OpenAI reasoning models routed via `/responses`.
|
|
33
|
+
- Free Groq keys have low per-minute token limits; large contexts may 429.
|
|
34
|
+
- For a fully local, no-key setup: install Ollama, `ollama pull qwen2.5-coder`,
|
|
35
|
+
then `polyclaude --ollama`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "polyclaude"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Use OpenAI, Gemini, and any OpenAI-compatible model inside the Claude Code CLI."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Devesh Pratap", email = "mail2deveshpratap@gmail.com" }]
|
|
13
|
+
keywords = ["claude-code", "claude", "openai", "gemini", "gpt", "llm", "cli", "anthropic", "proxy", "coding-agent"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Software Development :: Build Tools",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["mitmproxy>=10"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/ibedevesh/polyclaude"
|
|
26
|
+
Repository = "https://github.com/ibedevesh/polyclaude"
|
|
27
|
+
Issues = "https://github.com/ibedevesh/polyclaude/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
polyclaude = "polyclaude.cli:main"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["src/polyclaude"]
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""
|
|
2
|
+
polyclaude bridge — a mitmproxy addon that answers Claude Code's Anthropic
|
|
3
|
+
Messages API requests from an OpenAI-compatible backend (OpenAI, Gemini, Groq,
|
|
4
|
+
OpenRouter, Ollama, …), translating both directions on the wire.
|
|
5
|
+
|
|
6
|
+
Claude Code is unmodified. It sends its turn to api.anthropic.com/v1/messages;
|
|
7
|
+
this addon intercepts that request, translates it to the backend's format,
|
|
8
|
+
calls the backend, and translates the reply back into the exact Anthropic
|
|
9
|
+
streaming (SSE) grammar Claude Code expects.
|
|
10
|
+
|
|
11
|
+
Configured entirely via POLYCLAUDE_* environment variables (set by the CLI):
|
|
12
|
+
POLYCLAUDE_BASE backend base URL (…/v1)
|
|
13
|
+
POLYCLAUDE_MODEL main-loop model
|
|
14
|
+
POLYCLAUDE_SMALL model for the lightweight side-calls (titles etc.)
|
|
15
|
+
POLYCLAUDE_KEY API key for the backend
|
|
16
|
+
POLYCLAUDE_REASONING OpenAI reasoning effort: low|medium|high (default high)
|
|
17
|
+
POLYCLAUDE_AUTOCONTINUE 1 = stitch a turn truncated at the token cap
|
|
18
|
+
POLYCLAUDE_MAXTOK output cap (default 32768)
|
|
19
|
+
POLYCLAUDE_SYSTEM_APPEND_FILE append this file to the main system prompt
|
|
20
|
+
POLYCLAUDE_SYSTEM_REPLACE_FILE replace the main system prompt with this file
|
|
21
|
+
POLYCLAUDE_LOG optional path to a wire log
|
|
22
|
+
"""
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import ssl
|
|
26
|
+
import urllib.error
|
|
27
|
+
import urllib.request
|
|
28
|
+
|
|
29
|
+
from mitmproxy import http
|
|
30
|
+
|
|
31
|
+
BASE = os.environ.get("POLYCLAUDE_BASE", "").rstrip("/")
|
|
32
|
+
MODEL = os.environ.get("POLYCLAUDE_MODEL", "gpt-4.1")
|
|
33
|
+
SMALL = os.environ.get("POLYCLAUDE_SMALL", MODEL)
|
|
34
|
+
API_KEY = os.environ.get("POLYCLAUDE_KEY", "")
|
|
35
|
+
REASONING = os.environ.get("POLYCLAUDE_REASONING", "high")
|
|
36
|
+
AUTOCONT = os.environ.get("POLYCLAUDE_AUTOCONTINUE", "1") == "1"
|
|
37
|
+
MAX_TOK = int(os.environ.get("POLYCLAUDE_MAXTOK", "32768"))
|
|
38
|
+
MAXCONT = int(os.environ.get("POLYCLAUDE_MAXCONT", "4"))
|
|
39
|
+
LOG = os.environ.get("POLYCLAUDE_LOG", "")
|
|
40
|
+
|
|
41
|
+
_ssl_ctx = ssl.create_default_context()
|
|
42
|
+
try:
|
|
43
|
+
import certifi
|
|
44
|
+
_ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _read_file(var):
|
|
50
|
+
p = os.environ.get(var, "")
|
|
51
|
+
if not p:
|
|
52
|
+
return ""
|
|
53
|
+
try:
|
|
54
|
+
with open(os.path.expanduser(p)) as f:
|
|
55
|
+
return f.read().strip()
|
|
56
|
+
except Exception:
|
|
57
|
+
return ""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
SYS_APPEND = _read_file("POLYCLAUDE_SYSTEM_APPEND_FILE")
|
|
61
|
+
SYS_REPLACE = _read_file("POLYCLAUDE_SYSTEM_REPLACE_FILE")
|
|
62
|
+
|
|
63
|
+
# per-tool-call state that must round-trip across turns
|
|
64
|
+
_SIG = {} # Gemini 3.x thought_signature, keyed by tool-call id
|
|
65
|
+
_RSN = {} # OpenAI /responses reasoning items, keyed by tool-call id
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _log(m):
|
|
69
|
+
if LOG:
|
|
70
|
+
try:
|
|
71
|
+
with open(LOG, "a") as f:
|
|
72
|
+
f.write(m + "\n")
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _is_messages(flow):
|
|
78
|
+
return (flow.request.pretty_host.endswith("api.anthropic.com")
|
|
79
|
+
and "/v1/messages" in flow.request.path
|
|
80
|
+
and not flow.request.path.endswith("count_tokens"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_count(flow):
|
|
84
|
+
return (flow.request.pretty_host.endswith("api.anthropic.com")
|
|
85
|
+
and flow.request.path.endswith("count_tokens"))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _pick_model(anthropic_model):
|
|
89
|
+
return SMALL if "haiku" in (anthropic_model or "") else MODEL
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _is_main_loop(body):
|
|
93
|
+
m = body.get("model", "")
|
|
94
|
+
tools = {t.get("name") for t in body.get("tools", []) if isinstance(t, dict)}
|
|
95
|
+
return ("opus" in m) or ("sonnet" in m) or ("Bash" in tools) or ("Edit" in tools)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _sys_to_str(system):
|
|
99
|
+
if isinstance(system, str):
|
|
100
|
+
return system
|
|
101
|
+
if isinstance(system, list):
|
|
102
|
+
return "\n\n".join(b.get("text", "") for b in system
|
|
103
|
+
if isinstance(b, dict) and b.get("type") == "text")
|
|
104
|
+
return ""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _apply_prompt_override(body):
|
|
108
|
+
"""Append/replace the main-loop system prompt from the configured files."""
|
|
109
|
+
if not (SYS_APPEND or SYS_REPLACE) or not _is_main_loop(body):
|
|
110
|
+
return
|
|
111
|
+
sysv = body.get("system")
|
|
112
|
+
if SYS_REPLACE:
|
|
113
|
+
body["system"] = SYS_REPLACE
|
|
114
|
+
elif SYS_APPEND:
|
|
115
|
+
if isinstance(sysv, str):
|
|
116
|
+
body["system"] = sysv + "\n\n" + SYS_APPEND
|
|
117
|
+
elif isinstance(sysv, list):
|
|
118
|
+
sysv.append({"type": "text", "text": "\n\n" + SYS_APPEND})
|
|
119
|
+
else:
|
|
120
|
+
body["system"] = SYS_APPEND
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# Anthropic request -> OpenAI chat/completions request
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
def _to_openai(body):
|
|
127
|
+
msgs = []
|
|
128
|
+
system = _sys_to_str(body.get("system", ""))
|
|
129
|
+
if system:
|
|
130
|
+
msgs.append({"role": "system", "content": system})
|
|
131
|
+
|
|
132
|
+
for m in body.get("messages", []):
|
|
133
|
+
role = m.get("role")
|
|
134
|
+
content = m.get("content")
|
|
135
|
+
if isinstance(content, str):
|
|
136
|
+
msgs.append({"role": role, "content": content})
|
|
137
|
+
continue
|
|
138
|
+
text_parts, tool_calls, tool_results = [], [], []
|
|
139
|
+
for blk in content or []:
|
|
140
|
+
if not isinstance(blk, dict):
|
|
141
|
+
continue
|
|
142
|
+
bt = blk.get("type")
|
|
143
|
+
if bt == "text":
|
|
144
|
+
text_parts.append(blk.get("text", ""))
|
|
145
|
+
elif bt == "tool_use":
|
|
146
|
+
tc = {"id": blk.get("id"), "type": "function",
|
|
147
|
+
"function": {"name": blk.get("name"),
|
|
148
|
+
"arguments": json.dumps(blk.get("input", {}))}}
|
|
149
|
+
sig = _SIG.get(blk.get("id"))
|
|
150
|
+
if sig:
|
|
151
|
+
tc["extra_content"] = {"google": {"thought_signature": sig}}
|
|
152
|
+
tool_calls.append(tc)
|
|
153
|
+
elif bt == "tool_result":
|
|
154
|
+
rc = blk.get("content")
|
|
155
|
+
if isinstance(rc, list):
|
|
156
|
+
rc = "".join(x.get("text", "") for x in rc
|
|
157
|
+
if isinstance(x, dict))
|
|
158
|
+
elif not isinstance(rc, str):
|
|
159
|
+
rc = json.dumps(rc)
|
|
160
|
+
tool_results.append((blk.get("tool_use_id"), rc or ""))
|
|
161
|
+
elif bt == "image":
|
|
162
|
+
text_parts.append("[image omitted]")
|
|
163
|
+
if role == "assistant":
|
|
164
|
+
am = {"role": "assistant", "content": "\n".join(text_parts) or None}
|
|
165
|
+
if tool_calls:
|
|
166
|
+
am["tool_calls"] = tool_calls
|
|
167
|
+
msgs.append(am)
|
|
168
|
+
else:
|
|
169
|
+
for tid, rc in tool_results:
|
|
170
|
+
msgs.append({"role": "tool", "tool_call_id": tid, "content": rc})
|
|
171
|
+
if text_parts:
|
|
172
|
+
msgs.append({"role": "user", "content": "\n".join(text_parts)})
|
|
173
|
+
|
|
174
|
+
tgt = _pick_model(body.get("model", ""))
|
|
175
|
+
out = {"model": tgt, "messages": msgs,
|
|
176
|
+
"max_completion_tokens": min(int(body.get("max_tokens", MAX_TOK)),
|
|
177
|
+
MAX_TOK)}
|
|
178
|
+
reasoning = tgt.startswith(("gpt-5", "gpt-6", "o1", "o3", "o4"))
|
|
179
|
+
if not reasoning:
|
|
180
|
+
if body.get("temperature") is not None:
|
|
181
|
+
out["temperature"] = body["temperature"]
|
|
182
|
+
if body.get("top_p") is not None:
|
|
183
|
+
out["top_p"] = body["top_p"]
|
|
184
|
+
if body.get("stop_sequences"):
|
|
185
|
+
out["stop"] = body["stop_sequences"]
|
|
186
|
+
|
|
187
|
+
fn_tools = [{"type": "function",
|
|
188
|
+
"function": {"name": t.get("name"),
|
|
189
|
+
"description": t.get("description", ""),
|
|
190
|
+
"parameters": t.get("input_schema",
|
|
191
|
+
{"type": "object"})}}
|
|
192
|
+
for t in (body.get("tools") or []) if "input_schema" in t]
|
|
193
|
+
if fn_tools:
|
|
194
|
+
out["tools"] = fn_tools
|
|
195
|
+
tc = body.get("tool_choice") or {}
|
|
196
|
+
tt = tc.get("type")
|
|
197
|
+
out["tool_choice"] = ("required" if tt == "any"
|
|
198
|
+
else {"type": "function",
|
|
199
|
+
"function": {"name": tc["name"]}}
|
|
200
|
+
if tt == "tool" and tc.get("name") else "auto")
|
|
201
|
+
return out
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
# Anthropic request -> OpenAI /responses request (tools + full reasoning)
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
def _to_responses(body):
|
|
208
|
+
inp, seen = [], set()
|
|
209
|
+
for m in body.get("messages", []):
|
|
210
|
+
role = m.get("role")
|
|
211
|
+
content = m.get("content")
|
|
212
|
+
if isinstance(content, str):
|
|
213
|
+
inp.append({"role": role, "content": content})
|
|
214
|
+
continue
|
|
215
|
+
buf = []
|
|
216
|
+
if role == "assistant":
|
|
217
|
+
for blk in content or []:
|
|
218
|
+
bt = blk.get("type") if isinstance(blk, dict) else None
|
|
219
|
+
if bt == "text":
|
|
220
|
+
buf.append(blk.get("text", ""))
|
|
221
|
+
elif bt == "tool_use":
|
|
222
|
+
if buf:
|
|
223
|
+
inp.append({"role": "assistant", "content": "\n".join(buf)})
|
|
224
|
+
buf = []
|
|
225
|
+
for r in _RSN.get(blk.get("id"), []):
|
|
226
|
+
rid = r.get("id")
|
|
227
|
+
if rid and rid not in seen:
|
|
228
|
+
inp.append(r)
|
|
229
|
+
seen.add(rid)
|
|
230
|
+
inp.append({"type": "function_call",
|
|
231
|
+
"call_id": blk.get("id"), "name": blk.get("name"),
|
|
232
|
+
"arguments": json.dumps(blk.get("input", {}))})
|
|
233
|
+
if buf:
|
|
234
|
+
inp.append({"role": "assistant", "content": "\n".join(buf)})
|
|
235
|
+
else:
|
|
236
|
+
for blk in content or []:
|
|
237
|
+
bt = blk.get("type") if isinstance(blk, dict) else None
|
|
238
|
+
if bt == "text":
|
|
239
|
+
buf.append(blk.get("text", ""))
|
|
240
|
+
elif bt == "image":
|
|
241
|
+
buf.append("[image omitted]")
|
|
242
|
+
elif bt == "tool_result":
|
|
243
|
+
rc = blk.get("content")
|
|
244
|
+
if isinstance(rc, list):
|
|
245
|
+
rc = "".join(x.get("text", "") for x in rc
|
|
246
|
+
if isinstance(x, dict))
|
|
247
|
+
elif not isinstance(rc, str):
|
|
248
|
+
rc = json.dumps(rc)
|
|
249
|
+
inp.append({"type": "function_call_output",
|
|
250
|
+
"call_id": blk.get("tool_use_id"),
|
|
251
|
+
"output": rc or ""})
|
|
252
|
+
if buf:
|
|
253
|
+
inp.append({"role": "user", "content": "\n".join(buf)})
|
|
254
|
+
|
|
255
|
+
out = {"model": _pick_model(body.get("model", "")), "input": inp,
|
|
256
|
+
"store": False, "reasoning": {"effort": REASONING},
|
|
257
|
+
"include": ["reasoning.encrypted_content"],
|
|
258
|
+
"max_output_tokens": min(int(body.get("max_tokens", MAX_TOK)),
|
|
259
|
+
MAX_TOK)}
|
|
260
|
+
instr = _sys_to_str(body.get("system", ""))
|
|
261
|
+
if instr:
|
|
262
|
+
out["instructions"] = instr
|
|
263
|
+
fn = [{"type": "function", "name": t.get("name"),
|
|
264
|
+
"description": t.get("description", ""),
|
|
265
|
+
"parameters": t.get("input_schema", {"type": "object"})}
|
|
266
|
+
for t in (body.get("tools") or []) if "input_schema" in t]
|
|
267
|
+
if fn:
|
|
268
|
+
out["tools"] = fn
|
|
269
|
+
tc = body.get("tool_choice") or {}
|
|
270
|
+
tt = tc.get("type")
|
|
271
|
+
out["tool_choice"] = ("required" if tt == "any"
|
|
272
|
+
else {"type": "function", "name": tc["name"]}
|
|
273
|
+
if tt == "tool" and tc.get("name") else "auto")
|
|
274
|
+
return out
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _use_responses(tgt):
|
|
278
|
+
return "openai.com" in BASE and (
|
|
279
|
+
os.environ.get("POLYCLAUDE_RESPONSES_FORCE") == "1"
|
|
280
|
+
or tgt.startswith("gpt-5.6") or tgt.startswith("gpt-5.5"))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
# HTTP to the backend
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
def _post(path, payload, timeout):
|
|
287
|
+
req = urllib.request.Request(
|
|
288
|
+
f"{BASE}/{path}", data=json.dumps(payload).encode(),
|
|
289
|
+
headers={"Authorization": f"Bearer {API_KEY}",
|
|
290
|
+
"Content-Type": "application/json",
|
|
291
|
+
"Accept": "application/json",
|
|
292
|
+
"User-Agent": "polyclaude/0.1"},
|
|
293
|
+
method="POST")
|
|
294
|
+
with urllib.request.urlopen(req, timeout=timeout, context=_ssl_ctx) as r:
|
|
295
|
+
return json.loads(r.read())
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _chat(payload):
|
|
299
|
+
oai = _post("chat/completions", payload, 120)
|
|
300
|
+
if not AUTOCONT:
|
|
301
|
+
return oai
|
|
302
|
+
for _ in range(MAXCONT):
|
|
303
|
+
ch = (oai.get("choices") or [{}])[0]
|
|
304
|
+
msg = ch.get("message", {})
|
|
305
|
+
if ch.get("finish_reason") != "length" or msg.get("tool_calls"):
|
|
306
|
+
break
|
|
307
|
+
cont = dict(payload)
|
|
308
|
+
cont["messages"] = payload["messages"] + [
|
|
309
|
+
{"role": "assistant", "content": msg.get("content") or ""},
|
|
310
|
+
{"role": "user", "content": "continue"}]
|
|
311
|
+
nxt = _post("chat/completions", cont, 120)
|
|
312
|
+
nmsg = (nxt.get("choices") or [{}])[0].get("message", {})
|
|
313
|
+
oai["choices"][0]["message"]["content"] = \
|
|
314
|
+
(msg.get("content") or "") + (nmsg.get("content") or "")
|
|
315
|
+
oai["choices"][0]["finish_reason"] = \
|
|
316
|
+
(nxt.get("choices") or [{}])[0].get("finish_reason")
|
|
317
|
+
if nmsg.get("tool_calls"):
|
|
318
|
+
oai["choices"][0]["message"]["tool_calls"] = nmsg["tool_calls"]
|
|
319
|
+
payload = cont
|
|
320
|
+
return oai
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _responses_to_oai(resp):
|
|
324
|
+
text, tool_calls, pending = "", [], []
|
|
325
|
+
for it in resp.get("output", []):
|
|
326
|
+
t = it.get("type")
|
|
327
|
+
if t == "reasoning":
|
|
328
|
+
pending.append(it)
|
|
329
|
+
elif t == "message":
|
|
330
|
+
for c in it.get("content", []):
|
|
331
|
+
if c.get("type") == "output_text":
|
|
332
|
+
text += c.get("text", "")
|
|
333
|
+
pending = []
|
|
334
|
+
elif t == "function_call":
|
|
335
|
+
cid = it.get("call_id") or it.get("id")
|
|
336
|
+
tool_calls.append({"id": cid, "type": "function",
|
|
337
|
+
"function": {"name": it.get("name"),
|
|
338
|
+
"arguments": it.get("arguments") or "{}"}})
|
|
339
|
+
if pending:
|
|
340
|
+
_RSN[cid] = list(pending)
|
|
341
|
+
reason = (resp.get("incomplete_details") or {}).get("reason")
|
|
342
|
+
finish = ("tool_calls" if tool_calls
|
|
343
|
+
else "length" if reason == "max_output_tokens" else "stop")
|
|
344
|
+
u = resp.get("usage", {})
|
|
345
|
+
return {"id": resp.get("id", "msg"),
|
|
346
|
+
"choices": [{"message": {"content": text, "tool_calls": tool_calls},
|
|
347
|
+
"finish_reason": finish}],
|
|
348
|
+
"usage": {"prompt_tokens": u.get("input_tokens", 0),
|
|
349
|
+
"completion_tokens": u.get("output_tokens", 0)}}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# ---------------------------------------------------------------------------
|
|
353
|
+
# OpenAI response -> Anthropic SSE
|
|
354
|
+
# ---------------------------------------------------------------------------
|
|
355
|
+
def _sse(event, data):
|
|
356
|
+
return f"event: {event}\ndata: {json.dumps(data)}\n\n".encode()
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
_STOP = {"stop": "end_turn", "length": "max_tokens",
|
|
360
|
+
"tool_calls": "tool_use", "content_filter": "end_turn"}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _to_sse(oai, anthropic_model):
|
|
364
|
+
ch = (oai.get("choices") or [{}])[0]
|
|
365
|
+
msg = ch.get("message", {})
|
|
366
|
+
text = msg.get("content") or ""
|
|
367
|
+
tool_calls = msg.get("tool_calls") or []
|
|
368
|
+
finish = ch.get("finish_reason", "stop")
|
|
369
|
+
usage = oai.get("usage", {})
|
|
370
|
+
out = b""
|
|
371
|
+
out += _sse("message_start", {"type": "message_start", "message": {
|
|
372
|
+
"id": oai.get("id", "msg"), "type": "message", "role": "assistant",
|
|
373
|
+
"model": anthropic_model, "content": [], "stop_reason": None,
|
|
374
|
+
"stop_sequence": None,
|
|
375
|
+
"usage": {"input_tokens": usage.get("prompt_tokens", 0),
|
|
376
|
+
"output_tokens": 0}}})
|
|
377
|
+
out += _sse("ping", {"type": "ping"})
|
|
378
|
+
idx = 0
|
|
379
|
+
if text:
|
|
380
|
+
out += _sse("content_block_start", {"type": "content_block_start",
|
|
381
|
+
"index": idx, "content_block": {"type": "text", "text": ""}})
|
|
382
|
+
out += _sse("content_block_delta", {"type": "content_block_delta",
|
|
383
|
+
"index": idx,
|
|
384
|
+
"delta": {"type": "text_delta", "text": text}})
|
|
385
|
+
out += _sse("content_block_stop",
|
|
386
|
+
{"type": "content_block_stop", "index": idx})
|
|
387
|
+
idx += 1
|
|
388
|
+
for tc in tool_calls:
|
|
389
|
+
fn = tc.get("function", {})
|
|
390
|
+
tid = tc.get("id", f"toolu_{idx}")
|
|
391
|
+
sig = (((tc.get("extra_content") or {}).get("google") or {})
|
|
392
|
+
.get("thought_signature"))
|
|
393
|
+
if sig:
|
|
394
|
+
_SIG[tid] = sig
|
|
395
|
+
out += _sse("content_block_start", {"type": "content_block_start",
|
|
396
|
+
"index": idx,
|
|
397
|
+
"content_block": {"type": "tool_use", "id": tid,
|
|
398
|
+
"name": fn.get("name"), "input": {}}})
|
|
399
|
+
out += _sse("content_block_delta", {"type": "content_block_delta",
|
|
400
|
+
"index": idx,
|
|
401
|
+
"delta": {"type": "input_json_delta",
|
|
402
|
+
"partial_json": fn.get("arguments") or "{}"}})
|
|
403
|
+
out += _sse("content_block_stop",
|
|
404
|
+
{"type": "content_block_stop", "index": idx})
|
|
405
|
+
idx += 1
|
|
406
|
+
stop = "tool_use" if tool_calls else _STOP.get(finish, "end_turn")
|
|
407
|
+
out += _sse("message_delta", {"type": "message_delta",
|
|
408
|
+
"delta": {"stop_reason": stop, "stop_sequence": None},
|
|
409
|
+
"usage": {"output_tokens": usage.get("completion_tokens", 0)}})
|
|
410
|
+
out += _sse("message_stop", {"type": "message_stop"})
|
|
411
|
+
return out
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _error_sse(model, message):
|
|
415
|
+
return _to_sse({"id": "err", "choices": [{"message": {
|
|
416
|
+
"content": f"[polyclaude error] {message}"},
|
|
417
|
+
"finish_reason": "stop"}], "usage": {}}, model)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# ---------------------------------------------------------------------------
|
|
421
|
+
# mitmproxy hook
|
|
422
|
+
# ---------------------------------------------------------------------------
|
|
423
|
+
class Bridge:
|
|
424
|
+
def request(self, flow: http.HTTPFlow):
|
|
425
|
+
if _is_count(flow):
|
|
426
|
+
try:
|
|
427
|
+
body = json.loads(flow.request.content or b"{}")
|
|
428
|
+
except Exception:
|
|
429
|
+
body = {}
|
|
430
|
+
flow.response = http.Response.make(
|
|
431
|
+
200, json.dumps({"input_tokens": len(json.dumps(body)) // 4}
|
|
432
|
+
).encode(), {"content-type": "application/json"})
|
|
433
|
+
return
|
|
434
|
+
if not _is_messages(flow):
|
|
435
|
+
return
|
|
436
|
+
try:
|
|
437
|
+
body = json.loads(flow.request.content or b"{}")
|
|
438
|
+
except Exception:
|
|
439
|
+
return
|
|
440
|
+
model = body.get("model", "claude-opus")
|
|
441
|
+
_apply_prompt_override(body)
|
|
442
|
+
tgt = _pick_model(model)
|
|
443
|
+
_log(f"\n=== {model} -> {tgt} tools={len(body.get('tools') or [])} "
|
|
444
|
+
f"msgs={len(body.get('messages', []))} ===")
|
|
445
|
+
try:
|
|
446
|
+
if _use_responses(tgt):
|
|
447
|
+
oai = _responses_to_oai(_post("responses", _to_responses(body), 300))
|
|
448
|
+
_log(f">>> via /responses (reasoning={REASONING})")
|
|
449
|
+
else:
|
|
450
|
+
oai = _chat(_to_openai(body))
|
|
451
|
+
ch = (oai.get("choices") or [{}])[0]
|
|
452
|
+
_log(f">>> finish={ch.get('finish_reason')} "
|
|
453
|
+
f"usage={oai.get('usage', {})}")
|
|
454
|
+
sse = _to_sse(oai, model)
|
|
455
|
+
except urllib.error.HTTPError as e:
|
|
456
|
+
detail = e.read().decode(errors="replace")[:400]
|
|
457
|
+
_log(f"!!! backend {e.code}: {detail}")
|
|
458
|
+
sse = _error_sse(model, f"backend {e.code}: {detail}")
|
|
459
|
+
except Exception as e:
|
|
460
|
+
_log(f"!!! {e!r}")
|
|
461
|
+
sse = _error_sse(model, repr(e))
|
|
462
|
+
flow.response = http.Response.make(
|
|
463
|
+
200, sse, {"content-type": "text/event-stream; charset=utf-8"})
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
addons = [Bridge()]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""polyclaude CLI — start the bridge and launch Claude Code on a chosen model."""
|
|
2
|
+
import argparse
|
|
3
|
+
import atexit
|
|
4
|
+
import importlib.util
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import socket
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .providers import PROVIDERS
|
|
15
|
+
|
|
16
|
+
CA = Path.home() / ".mitmproxy" / "mitmproxy-ca-cert.pem"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _die(msg, code=1):
|
|
20
|
+
print(f"polyclaude: {msg}", file=sys.stderr)
|
|
21
|
+
sys.exit(code)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _find(exe):
|
|
25
|
+
"""Locate an executable, preferring the one next to our interpreter."""
|
|
26
|
+
local = Path(sys.executable).parent / exe
|
|
27
|
+
if local.exists():
|
|
28
|
+
return str(local)
|
|
29
|
+
return shutil.which(exe)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _resolve_key(prov):
|
|
33
|
+
for var in prov["key_env"]:
|
|
34
|
+
v = os.environ.get(var)
|
|
35
|
+
if v:
|
|
36
|
+
return v
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _profile_path(name):
|
|
41
|
+
# a path wins; otherwise look up a bundled profile by name
|
|
42
|
+
if os.path.sep in name or name.endswith(".md"):
|
|
43
|
+
p = Path(name).expanduser()
|
|
44
|
+
return str(p) if p.exists() else None
|
|
45
|
+
bundled = Path(__file__).parent / "profiles" / f"{name}.md"
|
|
46
|
+
return str(bundled) if bundled.exists() else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _free_port(start):
|
|
50
|
+
for p in range(start, start + 40):
|
|
51
|
+
with socket.socket() as s:
|
|
52
|
+
if s.connect_ex(("127.0.0.1", p)) != 0:
|
|
53
|
+
return p
|
|
54
|
+
return start
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _wait_listen(port, timeout=25):
|
|
58
|
+
end = time.time() + timeout
|
|
59
|
+
while time.time() < end:
|
|
60
|
+
with socket.socket() as s:
|
|
61
|
+
if s.connect_ex(("127.0.0.1", port)) == 0:
|
|
62
|
+
return True
|
|
63
|
+
time.sleep(0.2)
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main(argv=None):
|
|
68
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
69
|
+
passthrough = []
|
|
70
|
+
if "--" in argv:
|
|
71
|
+
i = argv.index("--")
|
|
72
|
+
passthrough = argv[i + 1:]
|
|
73
|
+
argv = argv[:i]
|
|
74
|
+
|
|
75
|
+
ap = argparse.ArgumentParser(
|
|
76
|
+
prog="polyclaude",
|
|
77
|
+
description="Use OpenAI, Gemini, and any OpenAI-compatible model in Claude Code.",
|
|
78
|
+
epilog="Examples:\n"
|
|
79
|
+
" polyclaude --gemini\n"
|
|
80
|
+
" polyclaude --openai --model gpt-4.1\n"
|
|
81
|
+
" polyclaude --gemini --profile datascience\n"
|
|
82
|
+
" polyclaude --openai -- -p \"one-shot prompt\"",
|
|
83
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
84
|
+
g = ap.add_mutually_exclusive_group()
|
|
85
|
+
for name in PROVIDERS:
|
|
86
|
+
g.add_argument(f"--{name}", dest="provider", action="store_const",
|
|
87
|
+
const=name, help=f"use {name}")
|
|
88
|
+
ap.add_argument("--provider", dest="provider2",
|
|
89
|
+
help="provider name (alternative to the flags above)")
|
|
90
|
+
ap.add_argument("--model", help="model override")
|
|
91
|
+
ap.add_argument("--profile", help="specialize the system prompt "
|
|
92
|
+
"(bundled name or path to a .md file)")
|
|
93
|
+
ap.add_argument("--system", help="replace the whole main system prompt with this file")
|
|
94
|
+
ap.add_argument("--reasoning", choices=["low", "medium", "high"],
|
|
95
|
+
help="OpenAI reasoning depth (default high)")
|
|
96
|
+
ap.add_argument("--port", type=int, default=8118, help="proxy port (default 8118)")
|
|
97
|
+
ap.add_argument("--list", action="store_true", help="list providers and exit")
|
|
98
|
+
ap.add_argument("--verbose", action="store_true", help="print the wire log path")
|
|
99
|
+
ap.add_argument("--version", action="version",
|
|
100
|
+
version=f"polyclaude {__version__}")
|
|
101
|
+
args = ap.parse_args(argv)
|
|
102
|
+
|
|
103
|
+
if args.list:
|
|
104
|
+
for n, p in PROVIDERS.items():
|
|
105
|
+
key = (p["key_env"][0] if p["key_env"] else "(none)")
|
|
106
|
+
print(f" --{n:<11} default {p['model']:<28} key: {key}")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
provider = args.provider or args.provider2 or "gemini"
|
|
110
|
+
if provider not in PROVIDERS:
|
|
111
|
+
_die(f"unknown provider '{provider}'. Try: {', '.join(PROVIDERS)}")
|
|
112
|
+
prov = PROVIDERS[provider]
|
|
113
|
+
|
|
114
|
+
key = _resolve_key(prov)
|
|
115
|
+
if prov["key_env"] and not key:
|
|
116
|
+
_die(f"no API key found. Set one of {prov['key_env']} in your "
|
|
117
|
+
f"environment.\n {prov['help']}")
|
|
118
|
+
|
|
119
|
+
claude = shutil.which("claude")
|
|
120
|
+
if not claude:
|
|
121
|
+
_die("the `claude` CLI is not installed or not on PATH.\n"
|
|
122
|
+
" Install: https://docs.claude.com/claude-code")
|
|
123
|
+
mitmdump = _find("mitmdump")
|
|
124
|
+
if not mitmdump:
|
|
125
|
+
_die("mitmproxy not found (it is a dependency; try reinstalling polyclaude).")
|
|
126
|
+
|
|
127
|
+
bridge = importlib.util.find_spec("polyclaude.bridge").origin
|
|
128
|
+
model = args.model or prov["model"]
|
|
129
|
+
port = _free_port(args.port)
|
|
130
|
+
log = "/tmp/polyclaude.log"
|
|
131
|
+
|
|
132
|
+
env = os.environ.copy()
|
|
133
|
+
env.update({
|
|
134
|
+
"POLYCLAUDE_BASE": prov["base"],
|
|
135
|
+
"POLYCLAUDE_MODEL": model,
|
|
136
|
+
"POLYCLAUDE_SMALL": prov["small"],
|
|
137
|
+
"POLYCLAUDE_KEY": key or "none",
|
|
138
|
+
"POLYCLAUDE_REASONING": args.reasoning or "high",
|
|
139
|
+
"POLYCLAUDE_AUTOCONTINUE": "1",
|
|
140
|
+
"POLYCLAUDE_LOG": log,
|
|
141
|
+
})
|
|
142
|
+
if args.system:
|
|
143
|
+
sp = Path(args.system).expanduser()
|
|
144
|
+
if not sp.exists():
|
|
145
|
+
_die(f"--system file not found: {args.system}")
|
|
146
|
+
env["POLYCLAUDE_SYSTEM_REPLACE_FILE"] = str(sp)
|
|
147
|
+
elif args.profile:
|
|
148
|
+
pp = _profile_path(args.profile)
|
|
149
|
+
if not pp:
|
|
150
|
+
avail = ", ".join(sorted(
|
|
151
|
+
p.stem for p in (Path(__file__).parent / "profiles").glob("*.md")))
|
|
152
|
+
_die(f"unknown profile '{args.profile}'. Bundled: {avail} "
|
|
153
|
+
f"(or pass a path to a .md file)")
|
|
154
|
+
env["POLYCLAUDE_SYSTEM_APPEND_FILE"] = pp
|
|
155
|
+
|
|
156
|
+
# a proxy env must NOT leak into the proxy's own upstream call
|
|
157
|
+
for v in ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"):
|
|
158
|
+
env.pop(v, None)
|
|
159
|
+
|
|
160
|
+
print(f"polyclaude {__version__} · {provider} · {model}")
|
|
161
|
+
open(log, "w").close()
|
|
162
|
+
proxy = subprocess.Popen(
|
|
163
|
+
[mitmdump, "-q", "-s", bridge, "--listen-host", "127.0.0.1",
|
|
164
|
+
"--listen-port", str(port), "--allow-hosts", r"anthropic\.com"],
|
|
165
|
+
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
166
|
+
|
|
167
|
+
def _cleanup():
|
|
168
|
+
if proxy.poll() is None:
|
|
169
|
+
proxy.terminate()
|
|
170
|
+
try:
|
|
171
|
+
proxy.wait(timeout=5)
|
|
172
|
+
except Exception:
|
|
173
|
+
proxy.kill()
|
|
174
|
+
atexit.register(_cleanup)
|
|
175
|
+
|
|
176
|
+
if not _wait_listen(port):
|
|
177
|
+
_cleanup()
|
|
178
|
+
_die("the bridge proxy did not start in time.")
|
|
179
|
+
# mitmproxy creates its CA on first start; wait for it
|
|
180
|
+
for _ in range(50):
|
|
181
|
+
if CA.exists():
|
|
182
|
+
break
|
|
183
|
+
time.sleep(0.1)
|
|
184
|
+
if not CA.exists():
|
|
185
|
+
_cleanup()
|
|
186
|
+
_die("mitmproxy CA not found; run `mitmdump` once to generate it, then retry.")
|
|
187
|
+
|
|
188
|
+
if args.verbose:
|
|
189
|
+
print(f" wire log: tail -f {log}")
|
|
190
|
+
print(f" launching Claude Code (Ctrl-C to exit)\n")
|
|
191
|
+
|
|
192
|
+
run_env = os.environ.copy()
|
|
193
|
+
run_env["HTTPS_PROXY"] = f"http://127.0.0.1:{port}"
|
|
194
|
+
run_env["NODE_EXTRA_CA_CERTS"] = str(CA)
|
|
195
|
+
run_env.setdefault("ANTHROPIC_API_KEY", "sk-polyclaude-bridge")
|
|
196
|
+
cmd = [claude, *passthrough]
|
|
197
|
+
try:
|
|
198
|
+
rc = subprocess.call(cmd, env=run_env)
|
|
199
|
+
except KeyboardInterrupt:
|
|
200
|
+
rc = 130
|
|
201
|
+
_cleanup()
|
|
202
|
+
sys.exit(rc)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Data Science specialist
|
|
2
|
+
|
|
3
|
+
You are operating as a senior data scientist / ML engineer. Bias all work toward:
|
|
4
|
+
|
|
5
|
+
- **Stack**: pandas / polars, numpy, scikit-learn, statsmodels, PyTorch; notebooks
|
|
6
|
+
and reproducible scripts.
|
|
7
|
+
- **Inspect before you model**: check shapes, dtypes, missingness, duplicates,
|
|
8
|
+
class balance, and target leakage before touching a model — and say what you saw.
|
|
9
|
+
- **Reproducibility**: set random seeds, pin the split, log versions.
|
|
10
|
+
- **Sound statistics**: state assumptions; prefer effect sizes and confidence
|
|
11
|
+
intervals over bare p-values; avoid p-hacking and multiple-comparison traps.
|
|
12
|
+
- **Honest evaluation**: hold out data, cross-validate, pick the metric that
|
|
13
|
+
matches the goal, check calibration and error slices — never present train-set
|
|
14
|
+
performance as if it generalizes.
|
|
15
|
+
- **Performance**: vectorize; be memory-aware on large frames; prefer
|
|
16
|
+
chunking/streaming for big data.
|
|
17
|
+
- **Communication**: clear plots (labeled axes + units), short tradeoff notes,
|
|
18
|
+
runnable code. Never claim things about data you have not inspected.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Code reviewer
|
|
2
|
+
|
|
3
|
+
You are a senior code reviewer. When reviewing or writing code:
|
|
4
|
+
|
|
5
|
+
- Prioritize correctness, then readability, then performance.
|
|
6
|
+
- Flag real defects with the exact file:line and a concrete failure scenario
|
|
7
|
+
(inputs → wrong output), not vague concerns.
|
|
8
|
+
- Watch for: off-by-one and boundary bugs, error/exception handling, resource
|
|
9
|
+
leaks, race conditions, input validation, and security-sensitive sinks.
|
|
10
|
+
- Prefer the smallest change that fixes the issue; call out unnecessary
|
|
11
|
+
complexity and suggest simplifications.
|
|
12
|
+
- Distinguish blocking issues from nits, and say which is which.
|
|
13
|
+
- When you change code, match the surrounding style and add tests where they
|
|
14
|
+
meaningfully reduce risk.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Provider presets: base URL, default models, and which env var holds the key.
|
|
2
|
+
|
|
3
|
+
Defaults are broadly-available models. Override any with `--model NAME`, and
|
|
4
|
+
newer models (Gemini 3.x, GPT-5.x/5.6) work too — the bridge handles their
|
|
5
|
+
quirks automatically.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
PROVIDERS = {
|
|
9
|
+
"gemini": {
|
|
10
|
+
"base": "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
11
|
+
"model": "gemini-2.5-pro",
|
|
12
|
+
"small": "gemini-2.5-flash",
|
|
13
|
+
"key_env": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
|
14
|
+
"help": "Get a key at https://aistudio.google.com/apikey",
|
|
15
|
+
},
|
|
16
|
+
"openai": {
|
|
17
|
+
"base": "https://api.openai.com/v1",
|
|
18
|
+
"model": "gpt-4.1",
|
|
19
|
+
"small": "gpt-4.1-mini",
|
|
20
|
+
"key_env": ["OPENAI_API_KEY"],
|
|
21
|
+
"help": "Get a key at https://platform.openai.com/api-keys",
|
|
22
|
+
},
|
|
23
|
+
"groq": {
|
|
24
|
+
"base": "https://api.groq.com/openai/v1",
|
|
25
|
+
"model": "llama-3.3-70b-versatile",
|
|
26
|
+
"small": "llama-3.1-8b-instant",
|
|
27
|
+
"key_env": ["GROQ_API_KEY"],
|
|
28
|
+
"help": "Get a free key at https://console.groq.com/keys",
|
|
29
|
+
},
|
|
30
|
+
"openrouter": {
|
|
31
|
+
"base": "https://openrouter.ai/api/v1",
|
|
32
|
+
"model": "anthropic/claude-3.5-sonnet",
|
|
33
|
+
"small": "anthropic/claude-3.5-haiku",
|
|
34
|
+
"key_env": ["OPENROUTER_API_KEY"],
|
|
35
|
+
"help": "Get a key at https://openrouter.ai/keys",
|
|
36
|
+
},
|
|
37
|
+
"ollama": {
|
|
38
|
+
"base": "http://127.0.0.1:11434/v1",
|
|
39
|
+
"model": "qwen2.5-coder",
|
|
40
|
+
"small": "qwen2.5-coder",
|
|
41
|
+
"key_env": [], # local, no key
|
|
42
|
+
"help": "Install from https://ollama.com and `ollama pull qwen2.5-coder`",
|
|
43
|
+
},
|
|
44
|
+
}
|