Skip to content

Commit bb6c11b

Browse files
Merge pull request #9 from mnfst/feat/google-gemini
feat: support native Gemini requests
2 parents 1db80f4 + e08b47e commit bb6c11b

28 files changed

Lines changed: 745 additions & 86 deletions

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ node/src/core/heal-api.ts the only code that talks to the heal service
1515
node/src/core/engine.ts orchestration, and createAutofix(adapter)
1616
node/src/openai/ OpenAI's routes, /chat/completions + /responses
1717
node/src/anthropic/ Anthropic's routes, /messages
18+
node/src/google/ Google's native :generateContent routes
1819
```
1920

2021
`python/src/mnfst_autofix/core/` mirrors that, plus `attempt.py`, which holds

README.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Heal your failed LLM requests on-the-fly to avoid any downtime. Malformed parame
1111

1212
### Node / TypeScript
1313

14-
One package - compatible with every LLM SDK.
14+
One package for OpenAI, Anthropic, and native Google Gemini requests.
1515

1616
```bash
1717
npm install @mnfst/autofix
@@ -57,6 +57,18 @@ const message = await client.messages.create({
5757
});
5858
```
5959

60+
#### Vercel AI SDK - Google Gemini
61+
62+
```ts
63+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
64+
import { autofix } from '@mnfst/autofix';
65+
66+
const google = createGoogleGenerativeAI({
67+
apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
68+
fetch: autofix({ sendMessages: true }),
69+
});
70+
```
71+
6072
Other providers like OpenRouter uses the OpenAI SDK with its own `baseURL`:
6173

6274
```ts
@@ -72,7 +84,7 @@ const client = new OpenAI({
7284

7385
### Python
7486

75-
One package - compatible with every LLM SDK.
87+
One package for OpenAI, Anthropic, and native Google Gemini requests.
7688

7789
```bash
7890
pip install mnfst-autofix
@@ -161,10 +173,9 @@ schema bodies, identity fields, and credential fields stay local. The provider e
161173
endpoint origin and path, derived workspace id, and SDK source are also sent.
162174

163175
`autofix({ sendMessages: true })` in Node / `autofix(send_messages=True)` in
164-
Python (default off) opts the top-level `messages` array in, verbatim — for
165-
teams pointing at their own heal service who want the conversation visible next
166-
to the failure it caused. It is observability only: the heal API still cannot
167-
author or rewrite a message on the way back.
176+
Python (default off) opts the top-level `messages` or Gemini `contents` array in,
177+
verbatim. It is observability only: the heal API still cannot author or rewrite
178+
the conversation on the way back.
168179

169180
## See heals
170181

@@ -182,8 +193,8 @@ const client = new OpenAI({
182193

183194
| Package | Language | SDKs |
184195
|---|---|---|
185-
| [`@mnfst/autofix`](node) | Node / TypeScript | OpenAI, Anthropic |
186-
| [`mnfst-autofix`](python) | Python | OpenAI, Anthropic |
196+
| [`@mnfst/autofix`](node) | Node / TypeScript | OpenAI, Anthropic, Google Gemini |
197+
| [`mnfst-autofix`](python) | Python | OpenAI, Anthropic, Google Gemini |
187198

188199
## License
189200

SECURITY.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ endpoint, on whatever host your SDK is pointed at, and sends the failed request
1212
to a heal API. Worth knowing exactly what that means:
1313

1414
**Removed before anything is sent, and restored from your own copy on the
15-
replay:** `messages`, `input`, `instructions`, `system`, `tools`,
16-
`response_format`, `text.format`. The strip is a union across dialects, so a
17-
content field is removed on every API, not just the one that normally carries
18-
it. See `node/src/core/anonymize.ts` (about 100 lines) or
15+
replay:** `messages`, Gemini `contents`, `input`, `instructions`, system prompts,
16+
tools, and response schemas. The strip is a union across dialects, so a content
17+
field is removed on every API, not just the one that normally carries it. See
18+
`node/src/core/anonymize.ts` or
1919
`python/src/mnfst_autofix/core/anonymize.py`.
2020

2121
**Sent as-is:** the provider's error message. Healing depends on it, and

node/README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Autofix Node
22

3-
Self-healing `fetch()` for LLM SDKs — the [OpenAI SDK](https://github.com/openai/openai-node) and the [Anthropic SDK](https://github.com/anthropics/anthropic-sdk-typescript). When a request fails with a fixable request-shape error — a renamed parameter, a restricted value, a retired model — autofix repairs it at runtime and retries. Your code doesn't change; your users never see the failure.
3+
Self-healing `fetch()` for OpenAI, Anthropic, and native Google Gemini requests. When a request fails with a fixable request-shape error — a renamed parameter, a restricted value, a retired model — autofix repairs it at runtime and retries. Your code doesn't change; your users never see the failure.
44

55
## Installation
66

@@ -49,6 +49,16 @@ const message = await client.messages.create({
4949
});
5050
```
5151

52+
```ts
53+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
54+
import { autofix } from '@mnfst/autofix';
55+
56+
const google = createGoogleGenerativeAI({
57+
apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
58+
fetch: autofix({ sendMessages: true }),
59+
});
60+
```
61+
5262
OpenRouter uses the OpenAI SDK with its own `baseURL`:
5363

5464
```ts
@@ -95,9 +105,8 @@ schema bodies, identity fields, and credential fields stay local. The provider e
95105
endpoint origin and path, derived workspace id, and SDK source are also sent.
96106

97107
`autofix({ sendMessages: true })` (default `false`) opts the top-level `messages`
98-
array in, verbatim — for teams pointing at their own heal service who want the
99-
conversation visible next to the failure it caused. It is observability only:
100-
the heal API still cannot author or rewrite a message on the way back.
108+
or Gemini `contents` array in, verbatim. It is observability only: the heal API
109+
still cannot author or rewrite the conversation on the way back.
101110

102111
## See heals
103112

node/examples/ai-demo.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// E2E demo: Vercel AI SDK + autofix + the hosted heal API.
2-
// One autofix() fetch shared by BOTH providers — the root export routes by
2+
// One autofix() fetch shared by both providers — the root export routes by
33
// path. Each scenario would normally throw; with autofix it heals to a 200.
44
// Needs OPENAI_API_KEY and ANTHROPIC_API_KEY (via .env or the environment).
55
//
@@ -9,8 +9,8 @@
99
// the Responses API), so a bad *param* never reaches the provider through this
1010
// SDK and there is nothing for autofix to heal. The model id is the one field
1111
// the SDK forwards verbatim. The three scenarios below therefore cover the
12-
// three distinct paths in the root route table — /chat/completions, /responses
13-
// and /messages — which is the claim this demo exists to prove.
12+
// three distinct paths exercised here — /chat/completions, /responses and
13+
// /messages — which is the claim this demo exists to prove.
1414

1515
import { generateText } from 'ai';
1616
import { createOpenAI } from '@ai-sdk/openai';
@@ -26,7 +26,7 @@ const healingFetch = autofix({
2626
},
2727
});
2828

29-
// ONE fetch, two providers: routing is by path, not by import.
29+
// One fetch, two providers: routing is by path, not by import.
3030
const openai = createOpenAI({ fetch: healingFetch });
3131
const anthropic = createAnthropic({ fetch: healingFetch });
3232

node/package-lock.json

Lines changed: 53 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@mnfst/autofix",
3-
"version": "0.3.2",
4-
"description": "Self-healing fetch() for LLM SDKs — OpenAI SDK, Anthropic SDK, and the Vercel AI SDK. Fixes request-side failures at runtime (renamed params, restricted values, retired models) and retries. Anonymized: your prompts never travel in the request.",
3+
"version": "0.4.0",
4+
"description": "Self-healing fetch() for OpenAI, Anthropic, and native Google Gemini requests. Fixes request-side failures at runtime and retries once.",
55
"license": "MIT",
66
"type": "module",
77
"repository": {
@@ -13,6 +13,8 @@
1313
"keywords": [
1414
"openai",
1515
"anthropic",
16+
"google",
17+
"gemini",
1618
"vercel",
1719
"ai-sdk",
1820
"llm",
@@ -40,7 +42,7 @@
4042
"demo:anthropic": "tsx --env-file-if-exists=.env examples/anthropic-demo.ts",
4143
"lint": "eslint .",
4244
"prepublishOnly": "npm test && npm run build",
43-
"test": "tsx --test tests/*.test.ts",
45+
"test": "node scripts/run-tests.mjs",
4446
"typecheck": "tsc --noEmit",
4547
"test:package": "npm run build && node scripts/package-smoke.mjs",
4648
"test:coverage": "tsx --test --experimental-test-coverage --test-coverage-exclude='tests/**' --test-coverage-lines=100 --test-coverage-functions=100 --test-coverage-branches=95 tests/*.test.ts"
@@ -50,6 +52,7 @@
5052
},
5153
"devDependencies": {
5254
"@ai-sdk/anthropic": "^4.0.33",
55+
"@ai-sdk/google": "^4.0.45",
5356
"@ai-sdk/openai": "^4.0.32",
5457
"@anthropic-ai/sdk": "^0.120.0",
5558
"@eslint/js": "^10.0.1",

node/scripts/run-tests.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { spawnSync } from 'node:child_process';
2+
import { readdirSync } from 'node:fs';
3+
import { createRequire } from 'node:module';
4+
import { fileURLToPath } from 'node:url';
5+
6+
const require = createRequire(import.meta.url);
7+
const tsxCli = require.resolve('tsx/cli');
8+
const testsDirectory = new URL('../tests/', import.meta.url);
9+
const testFiles = readdirSync(testsDirectory)
10+
.filter((file) => file.endsWith('.test.ts'))
11+
.sort();
12+
13+
for (const testFile of testFiles) {
14+
const result = spawnSync(
15+
process.execPath,
16+
[tsxCli, fileURLToPath(new URL(testFile, testsDirectory))],
17+
{ stdio: 'inherit' },
18+
);
19+
20+
if (result.status !== 0) {
21+
process.exit(result.status ?? 1);
22+
}
23+
}

node/src/core/anonymize.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
* `user_location` is where the end user physically is.
2828
*/
2929
const NEVER_TRAVELS: ReadonlySet<string> = new Set([
30-
'content', 'input', 'instructions', 'system', 'prompt', 'stop', 'stop_sequences',
31-
'schema', 'json_schema',
32-
'user', 'metadata', 'safety_identifier', 'prompt_cache_key', 'user_location',
30+
'content', 'contents', 'input', 'instructions', 'system', 'systemInstruction',
31+
'prompt', 'stop', 'stop_sequences', 'stopSequences',
32+
'schema', 'json_schema', 'responseSchema',
33+
'user', 'metadata', 'labels', 'safety_identifier', 'prompt_cache_key', 'user_location',
34+
'cachedContent',
3335
'prediction', 'authorization_token',
3436
]);
3537

@@ -88,9 +90,9 @@ function sentObject(source: Record<string, unknown>): Record<string, unknown> {
8890
/** Caller-chosen relaxations of the boundary. Nothing here is on by default. */
8991
export interface StripOptions {
9092
/**
91-
* Send the top-level `messages` array to the heal API verbatim, content and
92-
* all. Off by default. For callers who run their own heal service and want
93-
* the conversation visible next to the failure it caused. Observability
93+
* Send the top-level `messages` or Gemini `contents` array to the heal API
94+
* verbatim, content and all. Off by default. For callers who want the
95+
* conversation visible next to the failure it caused. Observability
9496
* only: the merge below never consults this, so the heal API still cannot
9597
* author or rewrite a message even after being shown them.
9698
*/
@@ -104,6 +106,9 @@ export function stripRequest(request: Record<string, unknown>,
104106
if (options.sendMessages && Array.isArray(request.messages)) {
105107
kept.messages = structuredClone(request.messages);
106108
}
109+
if (options.sendMessages && Array.isArray(request.contents)) {
110+
kept.contents = structuredClone(request.contents);
111+
}
107112
return kept;
108113
}
109114

0 commit comments

Comments
 (0)