babelgate 0.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +467 -1
- package/dist/app.js +40 -0
- package/dist/cli.js +203 -0
- package/dist/config/env-config.js +99 -0
- package/dist/index.js +60 -0
- package/dist/middleware/error-handler.js +158 -0
- package/dist/middleware/request-logger.js +66 -0
- package/dist/middleware/require-api-key.js +80 -0
- package/dist/middleware/resolve-source.js +28 -0
- package/dist/routes/proxy.js +74 -0
- package/dist/services/http-client.js +220 -0
- package/dist/services/proxy.service.js +329 -0
- package/dist/services/stream-pipeline.js +119 -0
- package/dist/services/upstream-errors.js +34 -0
- package/dist/transformers/adapters/anthropic.adapter.js +292 -0
- package/dist/transformers/adapters/anthropic.messages.js +261 -0
- package/dist/transformers/adapters/anthropic.stream.js +378 -0
- package/dist/transformers/adapters/gemini.adapter.js +418 -0
- package/dist/transformers/adapters/gemini.schema.js +61 -0
- package/dist/transformers/adapters/gemini.stream.js +341 -0
- package/dist/transformers/adapters/gemini.tools.js +246 -0
- package/dist/transformers/adapters/index.js +141 -0
- package/dist/transformers/adapters/ollama.adapter.js +288 -0
- package/dist/transformers/adapters/ollama.stream.js +294 -0
- package/dist/transformers/adapters/ollama.tools.js +107 -0
- package/dist/transformers/adapters/openai.adapter.js +460 -0
- package/dist/transformers/adapters/openai.stream.js +229 -0
- package/dist/transformers/index.js +110 -0
- package/dist/types/anthropic.types.js +2 -0
- package/dist/types/canonical.schema.js +160 -0
- package/dist/types/canonical.types.js +8 -0
- package/dist/types/gemini.types.js +2 -0
- package/dist/types/ollama.types.js +4 -0
- package/dist/types/openai.types.js +2 -0
- package/dist/utils/canonical-error.js +77 -0
- package/dist/utils/errors.js +95 -0
- package/dist/utils/ids.js +17 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/model-map.js +39 -0
- package/dist/utils/sse.js +147 -0
- package/dist/utils/stream-reassemble.js +104 -0
- package/dist/utils/token-estimate.js +36 -0
- package/dist/utils/tool-args.js +26 -0
- package/package.json +54 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/NOTICE
ADDED
package/README.md
CHANGED
|
@@ -1 +1,467 @@
|
|
|
1
|
-
# babelgate
|
|
1
|
+
# babelgate
|
|
2
|
+
|
|
3
|
+
A small proxy that sits in front of LLM APIs and translates between their
|
|
4
|
+
request/response formats — streaming and tool calls included. Point your
|
|
5
|
+
OpenAI client at it, and it can talk to Gemini, Ollama, or Anthropic behind
|
|
6
|
+
the scenes — and vice versa. Point Claude Code at it, and it runs on any
|
|
7
|
+
tool-capable model you can reach.
|
|
8
|
+
|
|
9
|
+
Supported providers right now:
|
|
10
|
+
|
|
11
|
+
- OpenAI (and OpenAI-compatible backends like OpenRouter)
|
|
12
|
+
- Google Gemini
|
|
13
|
+
- Ollama
|
|
14
|
+
- Anthropic (Claude)
|
|
15
|
+
|
|
16
|
+
<!-- OWNER: add docs/assets/demo.gif (from the OpenRouter gate run) before merge.
|
|
17
|
+

|
|
18
|
+
-->
|
|
19
|
+
|
|
20
|
+
## Quick start: Claude Code on a local model
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
ollama pull qwen3-coder:30b
|
|
24
|
+
OLLAMA_API_BASE_URL=http://localhost:11434 \
|
|
25
|
+
npx babelgate --preset claude-code --target ollama --model qwen3-coder:30b
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
babelgate prints the two lines Claude Code needs:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
export ANTHROPIC_BASE_URL=http://localhost:3000
|
|
32
|
+
export ANTHROPIC_AUTH_TOKEN=babelgate
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Export them in your shell, then:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
claude
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Claude Code talks Anthropic format to babelgate; babelgate streams it to
|
|
42
|
+
Ollama — tools included. Two honest caveats on this exact path: the release
|
|
43
|
+
gate passed on hosted OpenRouter models, not on local `qwen3-coder:30b`, and
|
|
44
|
+
small local models call tools unreliably — see
|
|
45
|
+
[Known limitations](#known-limitations).
|
|
46
|
+
|
|
47
|
+
### Claude Code on OpenRouter (or any OpenAI-compatible backend)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
OPENAI_API_KEY=<your OpenRouter key> \
|
|
51
|
+
npx babelgate --preset claude-code --target openai \
|
|
52
|
+
--model qwen/qwen3-coder --base-url https://openrouter.ai/api
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The base URL is `https://openrouter.ai/api` — babelgate appends
|
|
56
|
+
`/v1/chat/completions` itself. If your OpenRouter key has a spend limit,
|
|
57
|
+
cap Claude Code's output budget or requests can be rejected:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export CLAUDE_CODE_MAX_OUTPUT_TOKENS=8000
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then the same two `export` lines and `claude`. Full walkthrough:
|
|
64
|
+
[docs/recipes/claude-code.md](docs/recipes/claude-code.md).
|
|
65
|
+
|
|
66
|
+
### Any OpenAI-SDK app on any backend
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
OLLAMA_API_BASE_URL=http://localhost:11434 \
|
|
70
|
+
npx babelgate --preset openai-apps --target ollama
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
prints:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
export OPENAI_BASE_URL=http://localhost:3000/v1
|
|
77
|
+
export OPENAI_API_KEY=babelgate
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The app sends whatever model name it likes — it's forwarded to the target
|
|
81
|
+
unchanged (send a name the target knows, like `qwen3-coder:30b`, or rename
|
|
82
|
+
with `MODEL_MAP`). Recipe: [docs/recipes/openai-sdk.md](docs/recipes/openai-sdk.md).
|
|
83
|
+
|
|
84
|
+
### Recommended tool-capable models
|
|
85
|
+
|
|
86
|
+
- **Verified through the release gate** (Claude Code completed a real edit
|
|
87
|
+
task through babelgate): `qwen/qwen3-coder` and `poolside/laguna-s-2.1:free`
|
|
88
|
+
on OpenRouter.
|
|
89
|
+
- **Local (Ollama):** `qwen3-coder:30b` is the intended launch model — it
|
|
90
|
+
needs serious RAM or a GPU. Smaller `qwen2.5-coder` sizes run on CPU but
|
|
91
|
+
are slow and call tools less reliably.
|
|
92
|
+
|
|
93
|
+
## The `babelgate` CLI
|
|
94
|
+
|
|
95
|
+
`npx babelgate` parses flags with `node:util`, sets environment defaults
|
|
96
|
+
that **your own env always overrides** (a stderr line,
|
|
97
|
+
`babelgate: your environment overrides <keys>`, tells you which preset
|
|
98
|
+
values lost), starts the server, and prints the client `export` lines to
|
|
99
|
+
paste. Upstream API keys come only from the environment — never from flags.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Usage: babelgate [--preset <claude-code|openai-apps>] [--target <provider>]
|
|
103
|
+
[--model <name>] [--base-url <url>] [--port <n>] [--help]
|
|
104
|
+
|
|
105
|
+
Upstream API keys come only from the environment
|
|
106
|
+
(OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, OLLAMA_API_KEY).
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| Flag | Effect |
|
|
110
|
+
| ------------- | ----------------------------------------------------------------------------- |
|
|
111
|
+
| `--preset` | `claude-code` (source `anthropic`) or `openai-apps` (source `openai`) |
|
|
112
|
+
| `--target` | `ollama`, `openai`, `anthropic` or `gemini` — sets `TARGET_API` |
|
|
113
|
+
| `--model` | `claude-code` preset only: `MODEL_MAP=claude-*=<model>` |
|
|
114
|
+
| `--base-url` | Sets `{TARGET}_API_BASE_URL` (e.g. `https://openrouter.ai/api` for OpenRouter) |
|
|
115
|
+
| `--port` | Listen port (default `3000`) |
|
|
116
|
+
| `--help` | Usage |
|
|
117
|
+
|
|
118
|
+
The `claude-code` preset also sets `TARGET_CONTEXT_WINDOW=32768` and, for
|
|
119
|
+
`--target ollama`, `UPSTREAM_TIMEOUT_MS=600000` — local models are slow.
|
|
120
|
+
Without `--model` it warns `babelgate: no --model given; claude-* model
|
|
121
|
+
names are forwarded to the target unchanged` on stderr.
|
|
122
|
+
|
|
123
|
+
Presets are just env values. Everything below works without them.
|
|
124
|
+
|
|
125
|
+
## How it works
|
|
126
|
+
|
|
127
|
+
The naive way to build this is a transformer per provider pair. That gets
|
|
128
|
+
painful fast — four providers means twelve conversion functions in each
|
|
129
|
+
direction, and every new provider adds another batch.
|
|
130
|
+
|
|
131
|
+
Instead, every request goes through a **canonical format** in the middle:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
OpenAI request ──► canonical ──► Gemini request
|
|
135
|
+
Gemini response ──► canonical ──► OpenAI response
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Each provider has an _adapter_ that knows how to convert its own format to and
|
|
139
|
+
from canonical. Adding a provider costs you one adapter file — the rest of the
|
|
140
|
+
pipeline doesn't care.
|
|
141
|
+
|
|
142
|
+
The actual request flow:
|
|
143
|
+
|
|
144
|
+
1. Request hits the Express app.
|
|
145
|
+
2. If the path matches the configured source provider's API prefix
|
|
146
|
+
(e.g. `/v1/chat/completions` for OpenAI), the proxy takes it.
|
|
147
|
+
Otherwise it falls through to a 404.
|
|
148
|
+
3. The body is converted to canonical, then from canonical to the target
|
|
149
|
+
format.
|
|
150
|
+
4. It's forwarded upstream to `{TARGET}_API_BASE_URL` + the target's
|
|
151
|
+
endpoint, with the right auth headers.
|
|
152
|
+
5. The response comes back through the same path in reverse.
|
|
153
|
+
|
|
154
|
+
Endpoint kinds:
|
|
155
|
+
|
|
156
|
+
- **Chat** — the request/response translation described above (OpenAI
|
|
157
|
+
`/v1/chat/completions`, Gemini `/v1beta/models/{m}:generateContent`,
|
|
158
|
+
Ollama `/api/chat`, Anthropic `/v1/messages`).
|
|
159
|
+
- **Streaming chat** — `stream: true` (and Gemini's
|
|
160
|
+
`:streamGenerateContent?alt=sse`) flows chunk-by-chunk in each side's own
|
|
161
|
+
wire protocol: SSE for OpenAI, Anthropic and Gemini, NDJSON for Ollama.
|
|
162
|
+
A client disconnect aborts the upstream request; an upstream that dies
|
|
163
|
+
mid-stream produces an in-stream error in the client's dialect, and a
|
|
164
|
+
stream that stalls longer than `UPSTREAM_STREAM_IDLE_MS` is cut rather
|
|
165
|
+
than left hanging.
|
|
166
|
+
- **Tools** — tool definitions, `tool_choice`, parallel calls, streamed
|
|
167
|
+
tool-call deltas and tool results map through canonical. Calls with
|
|
168
|
+
arguments that aren't valid JSON reach the model wrapped as
|
|
169
|
+
`{ "_invalid_arguments": "<raw>" }` instead of crashing the turn.
|
|
170
|
+
- **Model list** — GET endpoints (`/v1/models`, `/v1beta/models`,
|
|
171
|
+
`/api/tags`) transform the upstream catalog into the source format,
|
|
172
|
+
so client SDKs and dropdowns work unchanged.
|
|
173
|
+
- **`count_tokens`** (Anthropic source only) — answered locally as a
|
|
174
|
+
chars/4 estimate, marked by the `x-babelgate-token-estimate: true`
|
|
175
|
+
response header. Claude Code calls it constantly; the estimate keeps
|
|
176
|
+
it happy without a fake upstream.
|
|
177
|
+
|
|
178
|
+
Every completed chat request logs token usage (`inputTokens`,
|
|
179
|
+
`outputTokens`, `finishReason`, upstream latency) via pino — grep for
|
|
180
|
+
`"Request completed"`. Model-list requests log `modelCount` instead
|
|
181
|
+
(`"Models list completed"`).
|
|
182
|
+
|
|
183
|
+
## Configuration
|
|
184
|
+
|
|
185
|
+
Everything is environment variables. One instance serves one source→target
|
|
186
|
+
pair; run multiple instances for more.
|
|
187
|
+
|
|
188
|
+
| Variable | What it's for |
|
|
189
|
+
| ------------------------- | -------------------------------------------------------------------------------------------- |
|
|
190
|
+
| `SOURCE_API` | Format clients send to you — `openai`, `gemini`, `ollama`, or `anthropic` |
|
|
191
|
+
| `TARGET_API` | Format to translate to upstream — same options |
|
|
192
|
+
| `PORT` | Listen port (default `3000`) |
|
|
193
|
+
| `{PROVIDER}_API_BASE_URL` | Upstream base URL for the target provider, e.g. `OLLAMA_API_BASE_URL=http://localhost:11434` |
|
|
194
|
+
| `{PROVIDER}_API_KEY` | API key for the target provider (Ollama doesn't need one) |
|
|
195
|
+
| `UPSTREAM_TIMEOUT_MS` | Per-request upstream timeout (default `30000`) |
|
|
196
|
+
| `UPSTREAM_RETRIES` | Retries for transport failures / 429 / 503 (default `1`) |
|
|
197
|
+
| `UPSTREAM_STREAM_IDLE_MS` | Longest gap allowed between chunks mid-stream (default `300000`) |
|
|
198
|
+
| `MAX_BODY_SIZE` | JSON body limit (default `10mb` — base64 images need it) |
|
|
199
|
+
| `PROXY_API_KEY` | Optional client auth — when set, callers send the key in their provider's native auth header (see Authentication) |
|
|
200
|
+
| `MODEL_MAP` | Ordered `glob=model` rules, comma-separated — e.g. `claude-*haiku*=qwen2.5-coder:7b,claude-*=qwen3-coder:30b`. First match wins; replies still carry the name the client asked for |
|
|
201
|
+
| `TARGET_CONTEXT_WINDOW` | Minimum context window the target should allocate (Ollama `options.num_ctx`) |
|
|
202
|
+
|
|
203
|
+
Routing and upstream endpoints are conventions, not configuration: each
|
|
204
|
+
provider's routes (`kind`, `method`, `pathPrefix`, `requestPath`) live in
|
|
205
|
+
`PROVIDER_ROUTES` in `src/transformers/adapters/index.ts`.
|
|
206
|
+
|
|
207
|
+
## Authentication
|
|
208
|
+
|
|
209
|
+
Set `PROXY_API_KEY` and every proxied endpoint requires it, in whichever
|
|
210
|
+
native auth style the client SDK already sends — any one match
|
|
211
|
+
authenticates, and every comparison is timing-safe:
|
|
212
|
+
|
|
213
|
+
- `Authorization: Bearer <key>` — OpenAI and Ollama SDKs, Anthropic's
|
|
214
|
+
`ANTHROPIC_AUTH_TOKEN` (the `Bearer` scheme is case-insensitive)
|
|
215
|
+
- `x-api-key: <key>` — Anthropic SDK
|
|
216
|
+
- `x-goog-api-key: <key>` — Gemini SDK
|
|
217
|
+
- `?key=<key>` query parameter — Gemini's other auth style
|
|
218
|
+
|
|
219
|
+
A wrong or missing key gets a `401` in the source provider's error format.
|
|
220
|
+
The client key is never forwarded upstream — the upstream sees only the
|
|
221
|
+
proxy's own `{TARGET}_API_KEY`. Leave `PROXY_API_KEY` unset to disable
|
|
222
|
+
client auth (local dev — the CLI's printed `export …_TOKEN=babelgate`
|
|
223
|
+
lines are placeholders in that mode), and note `/health` stays open either
|
|
224
|
+
way.
|
|
225
|
+
|
|
226
|
+
## Errors
|
|
227
|
+
|
|
228
|
+
Errors on provider routes are rendered in the **source** provider's error
|
|
229
|
+
format, so client SDKs raise their native exceptions:
|
|
230
|
+
|
|
231
|
+
| Source | Error body |
|
|
232
|
+
|---|---|
|
|
233
|
+
| OpenAI | `{"error":{"message","type","param","code"}}` |
|
|
234
|
+
| Anthropic | `{"type":"error","error":{"type","message"}}` |
|
|
235
|
+
| Gemini | `{"error":{"code","message","status"}}` |
|
|
236
|
+
| Ollama | `{"error":"<message>"}` |
|
|
237
|
+
|
|
238
|
+
The same holds inside a stream: an upstream that fails mid-stream produces
|
|
239
|
+
the source dialect's in-stream error event, never silence or half a JSON
|
|
240
|
+
object.
|
|
241
|
+
|
|
242
|
+
When the upstream provider itself fails, the status the client sees is:
|
|
243
|
+
|
|
244
|
+
| Upstream | Client | Notes |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| 400, 422 | 400 | The upstream message passes through |
|
|
247
|
+
| 404 | 404 | Almost always "model not found" — the client can fix it |
|
|
248
|
+
| 413 | 413 | Request too large |
|
|
249
|
+
| 429 | 429 | `Retry-After` passed through when present |
|
|
250
|
+
| 401, 403 | 502 | `Upstream <provider> rejected the proxy's credentials (<status>)` — the *proxy's* upstream key failed, not the client's. Gemini's `400 API_KEY_INVALID` maps here too |
|
|
251
|
+
| 503, 529 | 503 | Unavailable |
|
|
252
|
+
| Other 5xx | 502 | Upstream failure |
|
|
253
|
+
| Timeout | 504 | After `UPSTREAM_TIMEOUT_MS` |
|
|
254
|
+
| Transport failure | 502 | Upstream unreachable |
|
|
255
|
+
| Non-JSON 2xx body | 502 | `Upstream returned a non-JSON response (content-type: <type>)` |
|
|
256
|
+
|
|
257
|
+
Error privacy in detail: proxy-generated messages contain no prompt text
|
|
258
|
+
or API keys, and client credentials are redacted from request logs. An
|
|
259
|
+
upstream 401/403 — including Gemini's `400 API_KEY_INVALID` — reaches
|
|
260
|
+
neither the client nor the logs; every other upstream error message is
|
|
261
|
+
passed through to the client and logged truncated to 200 characters.
|
|
262
|
+
Outside production, an error's `details` may include the first 200
|
|
263
|
+
characters of a non-JSON upstream body. Unknown paths and `/health` keep
|
|
264
|
+
a small generic error shape rather than any provider's.
|
|
265
|
+
|
|
266
|
+
## Running from source
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
npm ci
|
|
270
|
+
cp .env.example .env
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
For a local Ollama target, `.env` needs three lines:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
SOURCE_API=openai
|
|
277
|
+
TARGET_API=ollama
|
|
278
|
+
OLLAMA_API_BASE_URL=http://localhost:11434
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Pull a small model and start the proxy:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
ollama pull qwen2.5:0.5b
|
|
285
|
+
npm run dev
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Then send an OpenAI-style request at the proxy:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
curl http://localhost:3000/v1/chat/completions \
|
|
292
|
+
-H "Content-Type: application/json" \
|
|
293
|
+
-d '{"model": "qwen2.5:0.5b", "messages": [{"role": "user", "content": "Hello"}]}'
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
That gets translated to an Ollama `/api/chat` call upstream (the proxy
|
|
297
|
+
sends `stream: false` when you don't ask for a stream), and the response
|
|
298
|
+
comes back in OpenAI `chat.completion` shape. Add `"stream": true` and the
|
|
299
|
+
same call comes back as SSE.
|
|
300
|
+
|
|
301
|
+
## Project structure
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
src/
|
|
305
|
+
├── cli.ts npx babelgate — flags, presets, printed export lines
|
|
306
|
+
├── app.ts Express app wiring (middleware → routes → errors)
|
|
307
|
+
├── index.ts Server startup
|
|
308
|
+
├── config/env-config.ts Env var config (lazy, validated)
|
|
309
|
+
├── middleware/ pino request logging, error handler
|
|
310
|
+
├── routes/proxy.ts Thin HTTP adapter — writes streams, aborts on close
|
|
311
|
+
├── services/
|
|
312
|
+
│ ├── http-client.ts Upstream transport: timeout, retry, logging, postStream
|
|
313
|
+
│ ├── stream-pipeline.ts Provider-agnostic stream plumbing (abort, idle timeout)
|
|
314
|
+
│ ├── token-estimate.ts chars/4 estimate behind count_tokens
|
|
315
|
+
│ └── proxy.service.ts Orchestration: validate → transform → forward
|
|
316
|
+
├── transformers/
|
|
317
|
+
│ ├── index.ts transformRequest/transformResponse entry points
|
|
318
|
+
│ └── adapters/ One file per provider (+ .stream.ts codecs) + PROVIDER_ROUTES
|
|
319
|
+
├── types/
|
|
320
|
+
│ ├── canonical.types.ts The canonical format + CanonicalStreamEvent
|
|
321
|
+
│ ├── canonical.schema.ts Zod schemas for runtime validation
|
|
322
|
+
│ └── *.types.ts Provider types (mostly SDK re-exports)
|
|
323
|
+
└── utils/ logger (pino), error classes, SSE/NDJSON parsers,
|
|
324
|
+
ID generators, stream reassembly, MODEL_MAP
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Testing
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
npm test
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
All fast, no network. Layers:
|
|
334
|
+
|
|
335
|
+
- **Adapter unit tests** (`src/transformers/adapters/*.test.ts`) — each
|
|
336
|
+
adapter's to/from canonical functions.
|
|
337
|
+
- **Contract tests** (`tests/contracts/`) — the interesting part. Every
|
|
338
|
+
adapter's canonical output is validated against Zod schemas, fixture-driven
|
|
339
|
+
golden tests cover realistic payloads, stream decoders are run against a
|
|
340
|
+
corpus of recorded wire bytes (and re-fed byte-by-byte at every split
|
|
341
|
+
point) and parsed back with the providers' own SDKs, and property tests
|
|
342
|
+
hammer the event-ordering rules.
|
|
343
|
+
- **HTTP integration tests** (`tests/integration/`) — real requests against
|
|
344
|
+
the Express app via supertest, with upstream providers mocked by msw. No
|
|
345
|
+
actual network calls happen. `pairs.test.ts` generates end-to-end tests
|
|
346
|
+
per provider pair — chat and model list, so all 16 combinations get
|
|
347
|
+
exercised — `stream-pairs.test.ts` runs the streaming × tools matrix
|
|
348
|
+
over the launch pairs, and `errors.test.ts` runs every pair through the
|
|
349
|
+
upstream-error mapping table.
|
|
350
|
+
|
|
351
|
+
The contract tests are what make the adapter approach hold up: you don't need
|
|
352
|
+
N×N tests if every adapter produces schema-valid canonical data, because then
|
|
353
|
+
any pair composes by construction.
|
|
354
|
+
|
|
355
|
+
## Live tests
|
|
356
|
+
|
|
357
|
+
`npm test` never touches the network, so it can't catch upstream-format
|
|
358
|
+
drift. The opt-in live suite runs the real app against real upstreams:
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
npm run test:live
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
- **Ollama always runs**: `OLLAMA_API_BASE_URL` (default
|
|
365
|
+
`http://localhost:11434`) must be reachable or the suite fails loudly —
|
|
366
|
+
it never silently passes. Model from `LIVE_OLLAMA_MODEL` (default
|
|
367
|
+
`qwen2.5:0.5b`).
|
|
368
|
+
- **Paid providers are opt-in**: OpenAI, Anthropic and Gemini cases run
|
|
369
|
+
only when `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` or `GEMINI_API_KEY` are
|
|
370
|
+
set, with cheap defaults `gpt-4o-mini`, `claude-haiku-5-5` and
|
|
371
|
+
`gemini-3.8-flash` respectively — `LIVE_<PROVIDER>_MODEL` overrides.
|
|
372
|
+
**OpenRouter** runs as an extra OpenAI-style target when
|
|
373
|
+
`OPENROUTER_API_KEY` is set (model `LIVE_OPENROUTER_MODEL`, default
|
|
374
|
+
`qwen/qwen3-coder`). Skipped targets are listed in the output.
|
|
375
|
+
- Cases per available target, driven through every source format: a chat
|
|
376
|
+
round trip, the model list, an unknown model → 404 (400 on OpenRouter —
|
|
377
|
+
it answers bad model IDs differently), and a streamed chat asserted on
|
|
378
|
+
chunks, terminal event and usage — all on response *shape*, never on
|
|
379
|
+
generated text.
|
|
380
|
+
|
|
381
|
+
A nightly GitHub Actions job (`.github/workflows/live.yml`) runs the
|
|
382
|
+
suite against a fresh Ollama; paid-provider secrets are off until the
|
|
383
|
+
owner adds them.
|
|
384
|
+
|
|
385
|
+
**The release gate** — `scripts/gates/claude-code-edit.sh <ollama|openrouter>
|
|
386
|
+
[model]` — is the end-to-end proof: it copies a fixture repo with a failing
|
|
387
|
+
test, starts `babelgate --preset claude-code`, lets Claude Code fix the bug
|
|
388
|
+
through the proxy (file edits and `npm test` only), then runs the test
|
|
389
|
+
itself. Prints `GATE PASS`/`GATE FAIL`, the `claude --version`, model and
|
|
390
|
+
duration. It makes real network calls.
|
|
391
|
+
|
|
392
|
+
## Adding a provider
|
|
393
|
+
|
|
394
|
+
Adding a provider means one adapter plus registration, env typing and
|
|
395
|
+
fixtures/tests — see
|
|
396
|
+
[CONTRIBUTING.md](CONTRIBUTING.md#adding-a-provider).
|
|
397
|
+
|
|
398
|
+
## Known limitations
|
|
399
|
+
|
|
400
|
+
- **Claude Code ↔ Ollama has not been gate-tested on a capable model.**
|
|
401
|
+
The release gate passed on OpenRouter (`qwen/qwen3-coder`,
|
|
402
|
+
`poolside/laguna-s-2.1:free`), proving Claude Code end to end through
|
|
403
|
+
babelgate; the Ollama adapter is live-tested nightly, but the combined
|
|
404
|
+
path on `qwen3-coder:30b` was never run (accepted launch risk — small
|
|
405
|
+
machines can't fit the model). Local models may call tools unreliably,
|
|
406
|
+
and on CPU-only machines a slow model can stall past
|
|
407
|
+
`UPSTREAM_STREAM_IDLE_MS` (default 300 s) and have its stream cut.
|
|
408
|
+
- **Reasoning/thinking is not mapped.** `thinking` and
|
|
409
|
+
`redacted_thinking` blocks, thinking deltas, the `thinking` request
|
|
410
|
+
parameter and Ollama's `thinking` field are dropped (debug-logged by
|
|
411
|
+
type name only). `cache_control` and `metadata` are dropped too.
|
|
412
|
+
Reasoning mapping is planned for v1.4.
|
|
413
|
+
- **`parallel_tool_calls` is ignored** — providers get their own default
|
|
414
|
+
parallel behaviour.
|
|
415
|
+
- **Not every pair is equally verified.** Streaming and tools are
|
|
416
|
+
implemented in all four adapters, but the streaming × tools matrix only
|
|
417
|
+
gates the launch pairs: `anthropic → ollama`, `anthropic → openai`
|
|
418
|
+
(which is how OpenRouter is reached) and `openai → ollama`. The full
|
|
419
|
+
16-pair matrix is milestone 2 (v1.1.x). Gemini and Ollama **sources**
|
|
420
|
+
and the Gemini **target** have not had a clean live run yet.
|
|
421
|
+
- **`count_tokens` is an estimate** (chars/4), marked by the
|
|
422
|
+
`x-babelgate-token-estimate: true` header — enough for Claude Code's
|
|
423
|
+
budgeting, not for billing.
|
|
424
|
+
- **Ollama clients get a single JSON reply** when they omit `stream` —
|
|
425
|
+
unlike Ollama's own API, which streams NDJSON by default. Send
|
|
426
|
+
`"stream": false` explicitly to make that intent clear.
|
|
427
|
+
- **Non-chat Gemini methods return 501** — `:countTokens`, `:embedContent`
|
|
428
|
+
and friends aren't translated.
|
|
429
|
+
- Some provider-specific features (citations, extra usage fields) map to
|
|
430
|
+
canonical best-effort — the format only keeps what's expressible
|
|
431
|
+
across providers.
|
|
432
|
+
- One source→target pair per instance. That's a deliberate simplification,
|
|
433
|
+
not an oversight.
|
|
434
|
+
|
|
435
|
+
## Roadmap
|
|
436
|
+
|
|
437
|
+
The version-by-version plan — the 16-pair matrix in v1.1.x, one endpoint
|
|
438
|
+
for many providers in v1.2, and beyond — lives in
|
|
439
|
+
[docs/roadmap.md](docs/roadmap.md).
|
|
440
|
+
|
|
441
|
+
## Docker
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
docker build -t babelgate .
|
|
445
|
+
docker run -p 3000:3000 --env-file .env babelgate
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Release tags also publish `ghcr.io/dorrabbkhan/babelgate:<version>` and
|
|
449
|
+
`:latest` via `.github/workflows/release.yml`.
|
|
450
|
+
|
|
451
|
+
Multi-stage build: provider SDKs are `devDependencies` (type-only imports),
|
|
452
|
+
so the runtime image ships `dist/` plus the top-level runtime
|
|
453
|
+
dependencies from `package.json` — `dotenv`, `express`, `pino`,
|
|
454
|
+
`pino-http` and `zod`.
|
|
455
|
+
|
|
456
|
+
## Stack
|
|
457
|
+
|
|
458
|
+
Express 5, TypeScript, pino (logging), zod (schema validation), vitest +
|
|
459
|
+
msw + supertest (tests). Provider types come from the official SDKs wherever
|
|
460
|
+
possible.
|
|
461
|
+
|
|
462
|
+
## License
|
|
463
|
+
|
|
464
|
+
Apache-2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE). Contributions
|
|
465
|
+
are welcome: [CONTRIBUTING.md](CONTRIBUTING.md) covers setup and checks,
|
|
466
|
+
and first-time contributors sign the [CLA](CLA.md) once via the CLA
|
|
467
|
+
Assistant bot on their first PR.
|