babelgate 0.0.0 → 1.1.1

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.
Files changed (46) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +467 -1
  4. package/dist/app.js +40 -0
  5. package/dist/cli.js +203 -0
  6. package/dist/config/env-config.js +99 -0
  7. package/dist/index.js +60 -0
  8. package/dist/middleware/error-handler.js +158 -0
  9. package/dist/middleware/request-logger.js +66 -0
  10. package/dist/middleware/require-api-key.js +80 -0
  11. package/dist/middleware/resolve-source.js +28 -0
  12. package/dist/routes/proxy.js +74 -0
  13. package/dist/services/http-client.js +220 -0
  14. package/dist/services/proxy.service.js +329 -0
  15. package/dist/services/stream-pipeline.js +119 -0
  16. package/dist/services/upstream-errors.js +34 -0
  17. package/dist/transformers/adapters/anthropic.adapter.js +292 -0
  18. package/dist/transformers/adapters/anthropic.messages.js +261 -0
  19. package/dist/transformers/adapters/anthropic.stream.js +378 -0
  20. package/dist/transformers/adapters/gemini.adapter.js +418 -0
  21. package/dist/transformers/adapters/gemini.schema.js +61 -0
  22. package/dist/transformers/adapters/gemini.stream.js +341 -0
  23. package/dist/transformers/adapters/gemini.tools.js +246 -0
  24. package/dist/transformers/adapters/index.js +141 -0
  25. package/dist/transformers/adapters/ollama.adapter.js +288 -0
  26. package/dist/transformers/adapters/ollama.stream.js +294 -0
  27. package/dist/transformers/adapters/ollama.tools.js +107 -0
  28. package/dist/transformers/adapters/openai.adapter.js +460 -0
  29. package/dist/transformers/adapters/openai.stream.js +229 -0
  30. package/dist/transformers/index.js +110 -0
  31. package/dist/types/anthropic.types.js +2 -0
  32. package/dist/types/canonical.schema.js +160 -0
  33. package/dist/types/canonical.types.js +8 -0
  34. package/dist/types/gemini.types.js +2 -0
  35. package/dist/types/ollama.types.js +4 -0
  36. package/dist/types/openai.types.js +2 -0
  37. package/dist/utils/canonical-error.js +77 -0
  38. package/dist/utils/errors.js +95 -0
  39. package/dist/utils/ids.js +17 -0
  40. package/dist/utils/logger.js +34 -0
  41. package/dist/utils/model-map.js +39 -0
  42. package/dist/utils/sse.js +147 -0
  43. package/dist/utils/stream-reassemble.js +104 -0
  44. package/dist/utils/token-estimate.js +36 -0
  45. package/dist/utils/tool-args.js +26 -0
  46. package/package.json +55 -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
@@ -0,0 +1,2 @@
1
+ babelgate
2
+ Copyright 2025-2026 Dorrabb Khan
package/README.md CHANGED
@@ -1 +1,467 @@
1
- # babelgate — name reserved; first release coming soon. https://github.com/dorrabbkhan/llm-proxy
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
+ ![babelgate demo — Claude Code editing through the proxy](docs/assets/demo.gif)
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.