@neondatabase/ai-sdk-provider 0.0.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/README.md +90 -0
- package/package.json +24 -0
- package/src/index.ts +5 -0
- package/src/lib/neon-anthropic-language-model.ts +51 -0
- package/src/lib/neon-capabilities.ts +133 -0
- package/src/lib/neon-chat-language-model.ts +56 -0
- package/src/lib/neon-chat-options.ts +48 -0
- package/src/lib/neon-model-capabilities.test.ts +66 -0
- package/src/lib/neon-model-capabilities.ts +137 -0
- package/src/lib/neon-responses-language-model.ts +60 -0
- package/src/lib/provider.ts +211 -0
- package/src/lib/version.ts +2 -0
- package/src/v1.ts +26 -0
- package/tsconfig.json +4 -0
- package/tsdown.config.ts +12 -0
- package/vitest.config.ts +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @neondatabase/ai-sdk-provider
|
|
2
|
+
|
|
3
|
+
Community [Vercel AI SDK](https://ai-sdk.dev) provider for the [Neon](https://neon.com) AI Gateway.
|
|
4
|
+
|
|
5
|
+
The Neon AI Gateway is **branch-scoped**: each Neon project branch gets its own gateway host, and a platform token authorizes requests for that branch. This provider routes each model to the best gateway endpoint (Anthropic → native Messages, OpenAI → native Responses incl. **Codex**, everything else → unified OpenAI-compatible MLflow endpoint), so a single `neon('databricks-...')` call reaches the whole `databricks-*` catalog.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @neondatabase/ai-sdk-provider
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
The gateway URL is branch-scoped, so both values come from the Neon Console (your project → a branch → **AI Gateway** tab), or from `neonctl env pull` / `neon dev`:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
NEON_AI_GATEWAY_BASE_URL="https://<branch-id>-api.ai.<region>.aws.neon.tech"
|
|
19
|
+
NEON_AI_GATEWAY_TOKEN="nt_live_..."
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { neon } from "@neondatabase/ai-sdk-provider/v1";
|
|
26
|
+
import { generateText } from "ai";
|
|
27
|
+
|
|
28
|
+
// Reads NEON_AI_GATEWAY_BASE_URL + NEON_AI_GATEWAY_TOKEN from the environment.
|
|
29
|
+
const { text } = await generateText({
|
|
30
|
+
model: neon("databricks-claude-haiku-4-5"), // or 'databricks-gpt-5-3-codex', etc.
|
|
31
|
+
prompt: "Summarize Postgres for me.",
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or configure explicitly with `createNeon`:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { createNeon } from "@neondatabase/ai-sdk-provider/v1";
|
|
39
|
+
|
|
40
|
+
const neon = createNeon({
|
|
41
|
+
baseURL: process.env.NEON_AI_GATEWAY_BASE_URL,
|
|
42
|
+
apiKey: process.env.NEON_AI_GATEWAY_TOKEN,
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Routing
|
|
47
|
+
|
|
48
|
+
| Model family | Endpoint | Why |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| Anthropic (`databricks-claude-*`) | native Messages API | streaming structured output + native reasoning |
|
|
51
|
+
| OpenAI (`databricks-gpt-*`, `*-codex`) | native Responses API | Codex (native-only), native reasoning, image-gen tool |
|
|
52
|
+
| Everything else (Gemini, Llama, Qwen, gpt-oss, ...) | unified MLflow endpoint | broad coverage; Gemini is here because its native endpoint does not support streaming |
|
|
53
|
+
|
|
54
|
+
## Capabilities
|
|
55
|
+
|
|
56
|
+
Verified across Anthropic, OpenAI (incl. Codex), Google, and Meta models: `generateText` / `streamText` (text, system prompts, multi-turn), tool calling (single and multi-step, generate and stream), `generateObject` / `streamObject`, and image (vision) input.
|
|
57
|
+
|
|
58
|
+
For MLflow-routed models, the provider detects the model family and drops parameters a backend rejects (e.g. penalties/`seed` for Llama, `reasoningEffort` for Gemini) with an AI SDK warning (`result.warnings`) instead of failing the request.
|
|
59
|
+
|
|
60
|
+
## Image generation
|
|
61
|
+
|
|
62
|
+
Available on OpenAI models via the Responses `image_generation` tool (there is no `generateImage()` image-model endpoint). Use `streamText` — streaming returns the image as a `tool-result` part and avoids the gateway's non-streaming response-size cap and read timeout:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { streamText } from "ai";
|
|
66
|
+
import { neon } from "@neondatabase/ai-sdk-provider/v1";
|
|
67
|
+
import { imageGeneration } from "@ai-sdk/openai/internal";
|
|
68
|
+
|
|
69
|
+
const result = streamText({
|
|
70
|
+
model: neon("databricks-gpt-5-mini"),
|
|
71
|
+
prompt: "Generate an image of a red apple on a wooden table",
|
|
72
|
+
tools: { image: imageGeneration({ partialImages: 3 }) },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
for await (const part of result.fullStream) {
|
|
76
|
+
if (part.type === "tool-result" && "result" in part.output) {
|
|
77
|
+
const png = Buffer.from(part.output.result as string, "base64");
|
|
78
|
+
// save or use the image
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Limitations
|
|
84
|
+
|
|
85
|
+
- `generateImage()` and embeddings (`embed` / `embedMany`) are not offered by the gateway and throw `NoSuchModelError`.
|
|
86
|
+
- `gpt-oss-*` models return a non-standard ("harmony") response shape on the unified endpoint and are not fully supported.
|
|
87
|
+
|
|
88
|
+
## Versioning
|
|
89
|
+
|
|
90
|
+
Import from `@neondatabase/ai-sdk-provider/v1` to pin to a specific major. The default entry re-exports the latest stable version.
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neondatabase/ai-sdk-provider",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Community Vercel AI SDK provider for the Neon AI Gateway.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"neon",
|
|
7
|
+
"ai",
|
|
8
|
+
"ai-sdk",
|
|
9
|
+
"vercel-ai-sdk",
|
|
10
|
+
"provider",
|
|
11
|
+
"ai-gateway",
|
|
12
|
+
"llm",
|
|
13
|
+
"platform"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/neondatabase/neon-pkgs.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "Apache-2.0",
|
|
20
|
+
"author": {
|
|
21
|
+
"name": "Neon",
|
|
22
|
+
"url": "https://neon.com"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AnthropicLanguageModel } from "@ai-sdk/anthropic/internal";
|
|
2
|
+
import type {
|
|
3
|
+
LanguageModelV4,
|
|
4
|
+
LanguageModelV4CallOptions,
|
|
5
|
+
LanguageModelV4GenerateResult,
|
|
6
|
+
LanguageModelV4StreamResult,
|
|
7
|
+
} from "@ai-sdk/provider";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Language model for Anthropic models served via the Neon AI Gateway's native
|
|
11
|
+
* Messages API (`/ai-gateway/anthropic/v1/messages`). Unlocks streaming
|
|
12
|
+
* structured output and native reasoning for Claude.
|
|
13
|
+
*
|
|
14
|
+
* The shared Anthropic model defaults to fine-grained tool-input streaming
|
|
15
|
+
* (`eager_input_streaming: true`) on streaming tool calls, which the gateway
|
|
16
|
+
* rejects (`Extra inputs are not permitted`). We disable it via the model's own
|
|
17
|
+
* `toolStreaming` option so streaming tool calls work. Users can still override.
|
|
18
|
+
*/
|
|
19
|
+
export class NeonAnthropicLanguageModel
|
|
20
|
+
extends AnthropicLanguageModel
|
|
21
|
+
implements LanguageModelV4
|
|
22
|
+
{
|
|
23
|
+
private withGatewayCompat(
|
|
24
|
+
options: LanguageModelV4CallOptions,
|
|
25
|
+
): LanguageModelV4CallOptions {
|
|
26
|
+
const anthropic = options.providerOptions?.anthropic;
|
|
27
|
+
// Respect an explicit user setting.
|
|
28
|
+
if (anthropic != null && "toolStreaming" in anthropic) {
|
|
29
|
+
return options;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
...options,
|
|
33
|
+
providerOptions: {
|
|
34
|
+
...options.providerOptions,
|
|
35
|
+
anthropic: { ...anthropic, toolStreaming: false },
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override doGenerate(
|
|
41
|
+
options: LanguageModelV4CallOptions,
|
|
42
|
+
): Promise<LanguageModelV4GenerateResult> {
|
|
43
|
+
return super.doGenerate(this.withGatewayCompat(options));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override doStream(
|
|
47
|
+
options: LanguageModelV4CallOptions,
|
|
48
|
+
): Promise<LanguageModelV4StreamResult> {
|
|
49
|
+
return super.doStream(this.withGatewayCompat(options));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LanguageModelV4CallOptions,
|
|
3
|
+
LanguageModelV4StreamPart,
|
|
4
|
+
SharedV4ProviderOptions,
|
|
5
|
+
SharedV4Warning,
|
|
6
|
+
} from "@ai-sdk/provider";
|
|
7
|
+
import { isCustomReasoning } from "@ai-sdk/provider-utils";
|
|
8
|
+
import { getNeonModelCapabilities } from "./neon-model-capabilities.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Drop call options the resolved model's upstream backend does not accept and
|
|
12
|
+
* collect a warning for each, so callers get a clear signal instead of a hard
|
|
13
|
+
* `400` from the gateway.
|
|
14
|
+
*/
|
|
15
|
+
export function applyNeonCapabilities(
|
|
16
|
+
modelId: string,
|
|
17
|
+
options: LanguageModelV4CallOptions,
|
|
18
|
+
): { options: LanguageModelV4CallOptions; warnings: SharedV4Warning[] } {
|
|
19
|
+
const caps = getNeonModelCapabilities(modelId);
|
|
20
|
+
const warnings: SharedV4Warning[] = [];
|
|
21
|
+
const patch: Partial<LanguageModelV4CallOptions> = {};
|
|
22
|
+
|
|
23
|
+
const dropUnsupported = (feature: string) => {
|
|
24
|
+
warnings.push({
|
|
25
|
+
type: "unsupported",
|
|
26
|
+
feature,
|
|
27
|
+
details: `${feature} is not supported by the Neon AI Gateway for ${caps.family} model "${modelId}" and was dropped.`,
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (options.temperature != null && !caps.supportsTemperature) {
|
|
32
|
+
patch.temperature = undefined;
|
|
33
|
+
dropUnsupported("temperature");
|
|
34
|
+
}
|
|
35
|
+
if (options.topP != null && !caps.supportsTopP) {
|
|
36
|
+
patch.topP = undefined;
|
|
37
|
+
dropUnsupported("topP");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Anthropic-style models accept only one of temperature / topP.
|
|
41
|
+
const effectiveTemperature =
|
|
42
|
+
"temperature" in patch ? patch.temperature : options.temperature;
|
|
43
|
+
const effectiveTopP = "topP" in patch ? patch.topP : options.topP;
|
|
44
|
+
if (
|
|
45
|
+
caps.temperatureTopPMutuallyExclusive &&
|
|
46
|
+
effectiveTemperature != null &&
|
|
47
|
+
effectiveTopP != null
|
|
48
|
+
) {
|
|
49
|
+
patch.topP = undefined;
|
|
50
|
+
warnings.push({
|
|
51
|
+
type: "compatibility",
|
|
52
|
+
feature: "topP",
|
|
53
|
+
details: `${caps.family} models accept only one of temperature or topP; topP was dropped.`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (options.frequencyPenalty != null && !caps.supportsPenalties) {
|
|
58
|
+
patch.frequencyPenalty = undefined;
|
|
59
|
+
dropUnsupported("frequencyPenalty");
|
|
60
|
+
}
|
|
61
|
+
if (options.presencePenalty != null && !caps.supportsPenalties) {
|
|
62
|
+
patch.presencePenalty = undefined;
|
|
63
|
+
dropUnsupported("presencePenalty");
|
|
64
|
+
}
|
|
65
|
+
if (options.seed != null && !caps.supportsSeed) {
|
|
66
|
+
patch.seed = undefined;
|
|
67
|
+
dropUnsupported("seed");
|
|
68
|
+
}
|
|
69
|
+
if (options.stopSequences != null && !caps.supportsStopSequences) {
|
|
70
|
+
patch.stopSequences = undefined;
|
|
71
|
+
dropUnsupported("stopSequences");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!caps.supportsReasoningEffort) {
|
|
75
|
+
const { providerOptions } = options;
|
|
76
|
+
const hasProviderEffort =
|
|
77
|
+
providerOptions != null &&
|
|
78
|
+
Object.values(providerOptions).some(
|
|
79
|
+
(group) =>
|
|
80
|
+
"reasoningEffort" in group && group.reasoningEffort != null,
|
|
81
|
+
);
|
|
82
|
+
const hasReasoningOption =
|
|
83
|
+
isCustomReasoning(options.reasoning) &&
|
|
84
|
+
options.reasoning !== "none";
|
|
85
|
+
|
|
86
|
+
if (hasProviderEffort || hasReasoningOption) {
|
|
87
|
+
dropUnsupported("reasoningEffort");
|
|
88
|
+
if (hasProviderEffort && providerOptions != null) {
|
|
89
|
+
const cleaned: SharedV4ProviderOptions = {};
|
|
90
|
+
for (const [key, group] of Object.entries(providerOptions)) {
|
|
91
|
+
if ("reasoningEffort" in group) {
|
|
92
|
+
const { reasoningEffort: _removed, ...rest } = group;
|
|
93
|
+
cleaned[key] = rest;
|
|
94
|
+
} else {
|
|
95
|
+
cleaned[key] = group;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
patch.providerOptions = cleaned;
|
|
99
|
+
}
|
|
100
|
+
if (hasReasoningOption) {
|
|
101
|
+
patch.reasoning = undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (Object.keys(patch).length === 0) {
|
|
107
|
+
return { options, warnings };
|
|
108
|
+
}
|
|
109
|
+
return { options: { ...options, ...patch }, warnings };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Merge additional warnings into the `stream-start` part of a model stream.
|
|
114
|
+
*/
|
|
115
|
+
export function mergeStreamStartWarnings(extra: SharedV4Warning[]) {
|
|
116
|
+
let merged = false;
|
|
117
|
+
return new TransformStream<
|
|
118
|
+
LanguageModelV4StreamPart,
|
|
119
|
+
LanguageModelV4StreamPart
|
|
120
|
+
>({
|
|
121
|
+
transform(part, controller) {
|
|
122
|
+
if (!merged && part.type === "stream-start") {
|
|
123
|
+
merged = true;
|
|
124
|
+
controller.enqueue({
|
|
125
|
+
...part,
|
|
126
|
+
warnings: [...extra, ...part.warnings],
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
controller.enqueue(part);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { OpenAICompatibleChatLanguageModel } from "@ai-sdk/openai-compatible";
|
|
2
|
+
import type {
|
|
3
|
+
LanguageModelV4,
|
|
4
|
+
LanguageModelV4CallOptions,
|
|
5
|
+
LanguageModelV4GenerateResult,
|
|
6
|
+
LanguageModelV4StreamResult,
|
|
7
|
+
} from "@ai-sdk/provider";
|
|
8
|
+
import {
|
|
9
|
+
applyNeonCapabilities,
|
|
10
|
+
mergeStreamStartWarnings,
|
|
11
|
+
} from "./neon-capabilities.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Language model for the Neon AI Gateway unified (MLflow) endpoint.
|
|
15
|
+
*
|
|
16
|
+
* Used for every model that is not routed to a provider-native endpoint
|
|
17
|
+
* (Gemini, Llama, Qwen, gpt-oss, ...). It is a thin specialization of the shared
|
|
18
|
+
* OpenAI-compatible chat model that adds per-model capability handling:
|
|
19
|
+
* parameters an upstream backend is known to reject are dropped and reported as
|
|
20
|
+
* warnings instead of failing the request.
|
|
21
|
+
*/
|
|
22
|
+
export class NeonChatLanguageModel
|
|
23
|
+
extends OpenAICompatibleChatLanguageModel
|
|
24
|
+
implements LanguageModelV4
|
|
25
|
+
{
|
|
26
|
+
override async doGenerate(
|
|
27
|
+
options: LanguageModelV4CallOptions,
|
|
28
|
+
): Promise<LanguageModelV4GenerateResult> {
|
|
29
|
+
const { options: adjusted, warnings } = applyNeonCapabilities(
|
|
30
|
+
this.modelId,
|
|
31
|
+
options,
|
|
32
|
+
);
|
|
33
|
+
const result = await super.doGenerate(adjusted);
|
|
34
|
+
return warnings.length > 0
|
|
35
|
+
? { ...result, warnings: [...warnings, ...result.warnings] }
|
|
36
|
+
: result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override async doStream(
|
|
40
|
+
options: LanguageModelV4CallOptions,
|
|
41
|
+
): Promise<LanguageModelV4StreamResult> {
|
|
42
|
+
const { options: adjusted, warnings } = applyNeonCapabilities(
|
|
43
|
+
this.modelId,
|
|
44
|
+
options,
|
|
45
|
+
);
|
|
46
|
+
const result = await super.doStream(adjusted);
|
|
47
|
+
return warnings.length > 0
|
|
48
|
+
? {
|
|
49
|
+
...result,
|
|
50
|
+
stream: result.stream.pipeThrough(
|
|
51
|
+
mergeStreamStartWarnings(warnings),
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
: result;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Models served by the Neon AI Gateway. Anthropic and OpenAI models are routed
|
|
2
|
+
// to their provider-native endpoints; everything else uses the unified MLflow
|
|
3
|
+
// (OpenAI-compatible) endpoint.
|
|
4
|
+
//
|
|
5
|
+
// The authoritative, always-current catalog is shown in the Neon Console under
|
|
6
|
+
// the branch's "AI Gateway" tab. Any other id can be passed as a plain string
|
|
7
|
+
// via the `(string & {})` fallback.
|
|
8
|
+
export type NeonChatModelId =
|
|
9
|
+
// Anthropic (native Messages API)
|
|
10
|
+
| "databricks-claude-opus-4-8"
|
|
11
|
+
| "databricks-claude-opus-4-7"
|
|
12
|
+
| "databricks-claude-opus-4-6"
|
|
13
|
+
| "databricks-claude-opus-4-5"
|
|
14
|
+
| "databricks-claude-opus-4-1"
|
|
15
|
+
| "databricks-claude-sonnet-4-6"
|
|
16
|
+
| "databricks-claude-sonnet-4-5"
|
|
17
|
+
| "databricks-claude-sonnet-4"
|
|
18
|
+
| "databricks-claude-haiku-4-5"
|
|
19
|
+
// OpenAI (native Responses API, incl. Codex)
|
|
20
|
+
| "databricks-gpt-5"
|
|
21
|
+
| "databricks-gpt-5-mini"
|
|
22
|
+
| "databricks-gpt-5-nano"
|
|
23
|
+
| "databricks-gpt-5-1"
|
|
24
|
+
| "databricks-gpt-5-2"
|
|
25
|
+
| "databricks-gpt-5-2-codex"
|
|
26
|
+
| "databricks-gpt-5-3-codex"
|
|
27
|
+
| "databricks-gpt-5-4"
|
|
28
|
+
| "databricks-gpt-5-4-mini"
|
|
29
|
+
| "databricks-gpt-5-4-nano"
|
|
30
|
+
| "databricks-gpt-5-5"
|
|
31
|
+
| "databricks-gpt-5-5-pro"
|
|
32
|
+
// OpenAI open-weight (unified MLflow endpoint)
|
|
33
|
+
| "databricks-gpt-oss-120b"
|
|
34
|
+
| "databricks-gpt-oss-20b"
|
|
35
|
+
// Google (unified MLflow endpoint)
|
|
36
|
+
| "databricks-gemini-3-5-flash"
|
|
37
|
+
| "databricks-gemini-3-1-flash-lite"
|
|
38
|
+
| "databricks-gemini-2-5-pro"
|
|
39
|
+
| "databricks-gemini-2-5-flash"
|
|
40
|
+
| "databricks-gemma-3-12b"
|
|
41
|
+
// Meta (unified MLflow endpoint)
|
|
42
|
+
| "databricks-llama-4-maverick"
|
|
43
|
+
| "databricks-meta-llama-3-3-70b-instruct"
|
|
44
|
+
| "databricks-meta-llama-3-1-8b-instruct"
|
|
45
|
+
// Alibaba (unified MLflow endpoint)
|
|
46
|
+
| "databricks-qwen3-next-80b-a3b-instruct"
|
|
47
|
+
| "databricks-qwen35-122b-a10b"
|
|
48
|
+
| (string & {});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
getNeonModelCapabilities,
|
|
4
|
+
getNeonModelRoute,
|
|
5
|
+
} from "./neon-model-capabilities.js";
|
|
6
|
+
|
|
7
|
+
describe("getNeonModelRoute", () => {
|
|
8
|
+
it("routes Anthropic and OpenAI models to their native endpoints", () => {
|
|
9
|
+
expect(getNeonModelRoute("databricks-claude-opus-4-8")).toBe(
|
|
10
|
+
"anthropic",
|
|
11
|
+
);
|
|
12
|
+
expect(getNeonModelRoute("databricks-claude-haiku-4-5")).toBe(
|
|
13
|
+
"anthropic",
|
|
14
|
+
);
|
|
15
|
+
expect(getNeonModelRoute("databricks-gpt-5")).toBe("openai");
|
|
16
|
+
expect(getNeonModelRoute("databricks-gpt-5-mini")).toBe("openai");
|
|
17
|
+
expect(getNeonModelRoute("databricks-gpt-5-3-codex")).toBe("openai");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("falls back to the unified MLflow endpoint for everything else", () => {
|
|
21
|
+
// Gemini is routed to MLflow because its native endpoint cannot stream.
|
|
22
|
+
expect(getNeonModelRoute("databricks-gemini-2-5-flash")).toBe("mlflow");
|
|
23
|
+
expect(getNeonModelRoute("databricks-llama-4-maverick")).toBe("mlflow");
|
|
24
|
+
expect(getNeonModelRoute("databricks-qwen35-122b-a10b")).toBe("mlflow");
|
|
25
|
+
// gpt-oss is open-weight and served on the unified endpoint, not Responses.
|
|
26
|
+
expect(getNeonModelRoute("databricks-gpt-oss-120b")).toBe("mlflow");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("getNeonModelCapabilities", () => {
|
|
31
|
+
it("marks Anthropic models correctly", () => {
|
|
32
|
+
const caps = getNeonModelCapabilities("databricks-claude-haiku-4-5");
|
|
33
|
+
expect(caps.family).toBe("anthropic");
|
|
34
|
+
expect(caps.supportsPenalties).toBe(false);
|
|
35
|
+
expect(caps.supportsSeed).toBe(false);
|
|
36
|
+
expect(caps.temperatureTopPMutuallyExclusive).toBe(true);
|
|
37
|
+
expect(caps.supportsReasoningEffort).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("marks plain GPT-5 reasoning models without temperature/topP support", () => {
|
|
41
|
+
const caps = getNeonModelCapabilities("databricks-gpt-5-mini");
|
|
42
|
+
expect(caps.family).toBe("openai");
|
|
43
|
+
expect(caps.supportsTemperature).toBe(false);
|
|
44
|
+
expect(caps.supportsTopP).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("keeps temperature for gpt-5.1+ models", () => {
|
|
48
|
+
const caps = getNeonModelCapabilities("databricks-gpt-5-1");
|
|
49
|
+
expect(caps.supportsTemperature).toBe(true);
|
|
50
|
+
expect(caps.supportsTopP).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("marks Meta models without penalties/seed support", () => {
|
|
54
|
+
const caps = getNeonModelCapabilities("databricks-llama-4-maverick");
|
|
55
|
+
expect(caps.family).toBe("meta");
|
|
56
|
+
expect(caps.supportsPenalties).toBe(false);
|
|
57
|
+
expect(caps.supportsSeed).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("is permissive for unknown models", () => {
|
|
61
|
+
const caps = getNeonModelCapabilities("databricks-qwen35-122b-a10b");
|
|
62
|
+
expect(caps.family).toBe("other");
|
|
63
|
+
expect(caps.supportsPenalties).toBe(true);
|
|
64
|
+
expect(caps.supportsReasoningEffort).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export type NeonModelFamily =
|
|
2
|
+
| "anthropic"
|
|
3
|
+
| "google"
|
|
4
|
+
| "openai"
|
|
5
|
+
| "meta"
|
|
6
|
+
| "other";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Which gateway endpoint a model should be routed to.
|
|
10
|
+
*
|
|
11
|
+
* - `anthropic`: native Messages API — streaming structured output + native reasoning.
|
|
12
|
+
* - `openai`: native Responses API — models served only natively (e.g. Codex),
|
|
13
|
+
* native reasoning, and the image-generation tool.
|
|
14
|
+
* - `mlflow`: the unified, OpenAI-compatible endpoint (the fallback). Gemini is
|
|
15
|
+
* routed here because the gateway's native Gemini endpoint does not support
|
|
16
|
+
* streaming (`streamGenerateContent` is rejected).
|
|
17
|
+
*/
|
|
18
|
+
export type NeonModelRoute = "anthropic" | "openai" | "mlflow";
|
|
19
|
+
|
|
20
|
+
export function getNeonModelRoute(modelId: string): NeonModelRoute {
|
|
21
|
+
const id = modelId.toLowerCase();
|
|
22
|
+
if (id.includes("claude")) {
|
|
23
|
+
return "anthropic";
|
|
24
|
+
}
|
|
25
|
+
// OpenAI proprietary models (gpt-4/gpt-5 families, Codex) are served only via
|
|
26
|
+
// the native Responses API. `gpt-oss` is open-weight and served on the
|
|
27
|
+
// unified chat endpoint, so it is intentionally excluded here.
|
|
28
|
+
if (
|
|
29
|
+
(id.includes("gpt-") && !id.includes("gpt-oss")) ||
|
|
30
|
+
id.includes("codex")
|
|
31
|
+
) {
|
|
32
|
+
return "openai";
|
|
33
|
+
}
|
|
34
|
+
return "mlflow";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface NeonModelCapabilities {
|
|
38
|
+
family: NeonModelFamily;
|
|
39
|
+
supportsTemperature: boolean;
|
|
40
|
+
supportsTopP: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Whether `temperature` and `topP` may be sent together. Anthropic models
|
|
43
|
+
* accept only one of the two.
|
|
44
|
+
*/
|
|
45
|
+
temperatureTopPMutuallyExclusive: boolean;
|
|
46
|
+
supportsPenalties: boolean;
|
|
47
|
+
supportsSeed: boolean;
|
|
48
|
+
supportsStopSequences: boolean;
|
|
49
|
+
supportsReasoningEffort: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const PERMISSIVE: Omit<NeonModelCapabilities, "family"> = {
|
|
53
|
+
supportsTemperature: true,
|
|
54
|
+
supportsTopP: true,
|
|
55
|
+
temperatureTopPMutuallyExclusive: false,
|
|
56
|
+
supportsPenalties: true,
|
|
57
|
+
supportsSeed: true,
|
|
58
|
+
supportsStopSequences: true,
|
|
59
|
+
supportsReasoningEffort: true,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Heuristic, prefix-based capability detection for MLflow-routed Neon models.
|
|
64
|
+
*
|
|
65
|
+
* The unified endpoint proxies to heterogeneous upstream providers, each of
|
|
66
|
+
* which accepts a different subset of the OpenAI-style parameters the AI SDK
|
|
67
|
+
* emits. Sending a parameter an upstream rejects results in a hard `400`, so we
|
|
68
|
+
* strip the parameters a family is known to reject and surface a warning
|
|
69
|
+
* instead. Unknown/untested models stay permissive (passed through unchanged).
|
|
70
|
+
*/
|
|
71
|
+
export function getNeonModelCapabilities(
|
|
72
|
+
modelId: string,
|
|
73
|
+
): NeonModelCapabilities {
|
|
74
|
+
const id = modelId.toLowerCase();
|
|
75
|
+
|
|
76
|
+
// Anthropic (Claude): rejects penalties and seed, accepts only one of
|
|
77
|
+
// temperature/topP, and rejects the OpenAI `reasoning_effort` field.
|
|
78
|
+
if (id.includes("claude")) {
|
|
79
|
+
return {
|
|
80
|
+
family: "anthropic",
|
|
81
|
+
supportsTemperature: true,
|
|
82
|
+
supportsTopP: true,
|
|
83
|
+
temperatureTopPMutuallyExclusive: true,
|
|
84
|
+
supportsPenalties: false,
|
|
85
|
+
supportsSeed: false,
|
|
86
|
+
supportsStopSequences: true,
|
|
87
|
+
supportsReasoningEffort: false,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Google (Gemini): accepts standard sampling params but rejects the OpenAI
|
|
92
|
+
// `reasoning_effort` field (not part of Gemini's generation config).
|
|
93
|
+
if (id.includes("gemini")) {
|
|
94
|
+
return {
|
|
95
|
+
family: "google",
|
|
96
|
+
...PERMISSIVE,
|
|
97
|
+
supportsReasoningEffort: false,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// OpenAI GPT-5 reasoning family (routed to the native Responses API). The
|
|
102
|
+
// Responses model strips parameters the Responses API doesn't accept
|
|
103
|
+
// (penalties, seed, stop), but its reasoning-model detection matches the bare
|
|
104
|
+
// model id (`gpt-5`), which the gateway's `databricks-` prefix defeats. So we
|
|
105
|
+
// only handle the temperature/topP restriction here: the original gpt-5 /
|
|
106
|
+
// gpt-5-mini / gpt-5-nano require the default temperature and reject topP,
|
|
107
|
+
// while gpt-5.1+ (a minor version digit follows) accept them again.
|
|
108
|
+
if (/gpt-5/.test(id)) {
|
|
109
|
+
const hasMinorVersion = /gpt-5[.-]\d/.test(id);
|
|
110
|
+
return {
|
|
111
|
+
family: "openai",
|
|
112
|
+
supportsTemperature: hasMinorVersion,
|
|
113
|
+
supportsTopP: hasMinorVersion,
|
|
114
|
+
temperatureTopPMutuallyExclusive: false,
|
|
115
|
+
supportsPenalties: true,
|
|
116
|
+
supportsSeed: true,
|
|
117
|
+
supportsStopSequences: true,
|
|
118
|
+
supportsReasoningEffort: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Meta (Llama): rejects penalties and seed; accepts sampling params and stop.
|
|
123
|
+
if (id.includes("llama")) {
|
|
124
|
+
return {
|
|
125
|
+
family: "meta",
|
|
126
|
+
supportsTemperature: true,
|
|
127
|
+
supportsTopP: true,
|
|
128
|
+
temperatureTopPMutuallyExclusive: false,
|
|
129
|
+
supportsPenalties: false,
|
|
130
|
+
supportsSeed: false,
|
|
131
|
+
supportsStopSequences: true,
|
|
132
|
+
supportsReasoningEffort: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { family: "other", ...PERMISSIVE };
|
|
137
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { OpenAIResponsesLanguageModel } from "@ai-sdk/openai/internal";
|
|
2
|
+
import type {
|
|
3
|
+
LanguageModelV4,
|
|
4
|
+
LanguageModelV4CallOptions,
|
|
5
|
+
LanguageModelV4GenerateResult,
|
|
6
|
+
LanguageModelV4StreamResult,
|
|
7
|
+
} from "@ai-sdk/provider";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Language model for OpenAI models served via the Neon AI Gateway's native
|
|
11
|
+
* Responses API (`/ai-gateway/openai/v1/responses`). This route is required for
|
|
12
|
+
* models that are only served natively (e.g. Codex) and unlocks native
|
|
13
|
+
* reasoning and the image-generation tool.
|
|
14
|
+
*
|
|
15
|
+
* The shared OpenAI Responses model shapes a request based on whether the model
|
|
16
|
+
* is a reasoning model, but it detects that from the bare model id (`gpt-5`),
|
|
17
|
+
* which the gateway's required `databricks-` prefix defeats. For the GPT-5
|
|
18
|
+
* reasoning family we set the model's own `forceReasoning` provider option so it
|
|
19
|
+
* applies the correct reasoning behavior. Users can still override it.
|
|
20
|
+
*/
|
|
21
|
+
export class NeonResponsesLanguageModel
|
|
22
|
+
extends OpenAIResponsesLanguageModel
|
|
23
|
+
implements LanguageModelV4
|
|
24
|
+
{
|
|
25
|
+
private get isReasoningFamily(): boolean {
|
|
26
|
+
return /gpt-5/.test(this.modelId.toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private withForcedReasoning(
|
|
30
|
+
options: LanguageModelV4CallOptions,
|
|
31
|
+
): LanguageModelV4CallOptions {
|
|
32
|
+
if (!this.isReasoningFamily) {
|
|
33
|
+
return options;
|
|
34
|
+
}
|
|
35
|
+
const openai = options.providerOptions?.openai;
|
|
36
|
+
// Respect an explicit user setting.
|
|
37
|
+
if (openai != null && "forceReasoning" in openai) {
|
|
38
|
+
return options;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
...options,
|
|
42
|
+
providerOptions: {
|
|
43
|
+
...options.providerOptions,
|
|
44
|
+
openai: { ...openai, forceReasoning: true },
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override doGenerate(
|
|
50
|
+
options: LanguageModelV4CallOptions,
|
|
51
|
+
): Promise<LanguageModelV4GenerateResult> {
|
|
52
|
+
return super.doGenerate(this.withForcedReasoning(options));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override doStream(
|
|
56
|
+
options: LanguageModelV4CallOptions,
|
|
57
|
+
): Promise<LanguageModelV4StreamResult> {
|
|
58
|
+
return super.doStream(this.withForcedReasoning(options));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Community Vercel AI SDK provider for the Neon AI Gateway.
|
|
3
|
+
*
|
|
4
|
+
* `createNeon()` returns a provider that routes each model to the best gateway
|
|
5
|
+
* endpoint based on its id (Anthropic → native Messages, OpenAI → native
|
|
6
|
+
* Responses incl. Codex, everything else → unified MLflow), so a single
|
|
7
|
+
* `neon('databricks-...')` call (same base URL + token) reaches the whole
|
|
8
|
+
* catalog. Configure with the branch-scoped `NEON_AI_GATEWAY_BASE_URL` +
|
|
9
|
+
* `NEON_AI_GATEWAY_TOKEN` emitted by `neonctl env pull` / `neon dev`, or pass
|
|
10
|
+
* `baseURL` / `apiKey` explicitly.
|
|
11
|
+
*/
|
|
12
|
+
import type { ProviderErrorStructure } from "@ai-sdk/openai-compatible";
|
|
13
|
+
import {
|
|
14
|
+
type LanguageModelV4,
|
|
15
|
+
NoSuchModelError,
|
|
16
|
+
type ProviderV4,
|
|
17
|
+
} from "@ai-sdk/provider";
|
|
18
|
+
import {
|
|
19
|
+
type FetchFunction,
|
|
20
|
+
generateId,
|
|
21
|
+
loadApiKey,
|
|
22
|
+
loadSetting,
|
|
23
|
+
withoutTrailingSlash,
|
|
24
|
+
withUserAgentSuffix,
|
|
25
|
+
} from "@ai-sdk/provider-utils";
|
|
26
|
+
import { z } from "zod/v4";
|
|
27
|
+
import { NeonAnthropicLanguageModel } from "./neon-anthropic-language-model.js";
|
|
28
|
+
import { NeonChatLanguageModel } from "./neon-chat-language-model.js";
|
|
29
|
+
import type { NeonChatModelId } from "./neon-chat-options.js";
|
|
30
|
+
import { getNeonModelRoute } from "./neon-model-capabilities.js";
|
|
31
|
+
import { NeonResponsesLanguageModel } from "./neon-responses-language-model.js";
|
|
32
|
+
import { VERSION } from "./version.js";
|
|
33
|
+
|
|
34
|
+
const neonErrorSchema = z.object({
|
|
35
|
+
error: z.object({
|
|
36
|
+
message: z.string(),
|
|
37
|
+
type: z.string().nullish(),
|
|
38
|
+
param: z.unknown().nullish(),
|
|
39
|
+
code: z.unknown().nullish(),
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export type NeonErrorData = z.infer<typeof neonErrorSchema>;
|
|
44
|
+
|
|
45
|
+
const neonErrorStructure: ProviderErrorStructure<NeonErrorData> = {
|
|
46
|
+
errorSchema: neonErrorSchema,
|
|
47
|
+
errorToMessage: (data) => data.error.message,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Recursively remove the JSON Schema `$schema` marker, which some gateway
|
|
52
|
+
* backends (notably Gemini) reject in tool/structured-output schemas. Other
|
|
53
|
+
* backends ignore its absence, so stripping it everywhere is safe.
|
|
54
|
+
*/
|
|
55
|
+
function stripJsonSchemaMarker(value: unknown): unknown {
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
return value.map(stripJsonSchemaMarker);
|
|
58
|
+
}
|
|
59
|
+
if (value !== null && typeof value === "object") {
|
|
60
|
+
const result: Record<string, unknown> = {};
|
|
61
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
62
|
+
if (key === "$schema") {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
result[key] = stripJsonSchemaMarker(entry);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function transformNeonRequestBody(
|
|
73
|
+
args: Record<string, unknown>,
|
|
74
|
+
): Record<string, unknown> {
|
|
75
|
+
const transformed = { ...args };
|
|
76
|
+
if (transformed.tools != null) {
|
|
77
|
+
transformed.tools = stripJsonSchemaMarker(transformed.tools);
|
|
78
|
+
}
|
|
79
|
+
if (transformed.response_format != null) {
|
|
80
|
+
transformed.response_format = stripJsonSchemaMarker(
|
|
81
|
+
transformed.response_format,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return transformed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface NeonProviderSettings {
|
|
88
|
+
/**
|
|
89
|
+
* Neon AI Gateway base URL — the branch-scoped host root, e.g.
|
|
90
|
+
* `https://<branch-id>-api.ai.<region>.aws.neon.tech`. The gateway paths are
|
|
91
|
+
* appended internally. Falls back to the `NEON_AI_GATEWAY_BASE_URL` env var.
|
|
92
|
+
*/
|
|
93
|
+
baseURL?: string;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Neon AI Gateway platform token (the `nt_live_...` value). Falls back to the
|
|
97
|
+
* `NEON_AI_GATEWAY_TOKEN` env var.
|
|
98
|
+
*/
|
|
99
|
+
apiKey?: string;
|
|
100
|
+
|
|
101
|
+
/** Custom headers to include in the requests. */
|
|
102
|
+
headers?: Record<string, string>;
|
|
103
|
+
|
|
104
|
+
/** Custom fetch implementation (e.g. for testing or middleware). */
|
|
105
|
+
fetch?: FetchFunction;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface NeonProvider extends ProviderV4 {
|
|
109
|
+
/** Creates a Neon AI Gateway model for text generation. */
|
|
110
|
+
(modelId: NeonChatModelId): LanguageModelV4;
|
|
111
|
+
|
|
112
|
+
/** Creates a Neon AI Gateway model for text generation. */
|
|
113
|
+
languageModel(modelId: NeonChatModelId): LanguageModelV4;
|
|
114
|
+
|
|
115
|
+
/** Creates a Neon AI Gateway chat model for text generation. */
|
|
116
|
+
chat(modelId: NeonChatModelId): LanguageModelV4;
|
|
117
|
+
|
|
118
|
+
/** @deprecated Use `embeddingModel` instead. */
|
|
119
|
+
textEmbeddingModel(modelId: string): never;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createNeon(options: NeonProviderSettings = {}): NeonProvider {
|
|
123
|
+
// Resolved lazily so that `createNeon()` and the default `neon` instance do
|
|
124
|
+
// not throw at import time when configuration comes from the environment.
|
|
125
|
+
const getHost = () =>
|
|
126
|
+
withoutTrailingSlash(
|
|
127
|
+
loadSetting({
|
|
128
|
+
settingValue: options.baseURL,
|
|
129
|
+
environmentVariableName: "NEON_AI_GATEWAY_BASE_URL",
|
|
130
|
+
settingName: "baseURL",
|
|
131
|
+
description: "Neon AI Gateway base URL",
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const getHeaders = (extra?: Record<string, string>) =>
|
|
136
|
+
withUserAgentSuffix(
|
|
137
|
+
{
|
|
138
|
+
Authorization: `Bearer ${loadApiKey({
|
|
139
|
+
apiKey: options.apiKey,
|
|
140
|
+
environmentVariableName: "NEON_AI_GATEWAY_TOKEN",
|
|
141
|
+
description: "Neon AI Gateway token",
|
|
142
|
+
})}`,
|
|
143
|
+
...extra,
|
|
144
|
+
...options.headers,
|
|
145
|
+
},
|
|
146
|
+
`neondatabase/ai-sdk-provider/${VERSION}`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Anthropic models -> native Messages API.
|
|
150
|
+
const createAnthropicModel = (modelId: NeonChatModelId) =>
|
|
151
|
+
new NeonAnthropicLanguageModel(modelId, {
|
|
152
|
+
provider: "neon.anthropic",
|
|
153
|
+
baseURL: `${getHost()}/ai-gateway/anthropic/v1`,
|
|
154
|
+
headers: () => getHeaders({ "anthropic-version": "2023-06-01" }),
|
|
155
|
+
fetch: options.fetch,
|
|
156
|
+
generateId,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// OpenAI models (incl. Codex, only served natively) -> Responses API.
|
|
160
|
+
const createOpenAIModel = (modelId: NeonChatModelId) =>
|
|
161
|
+
new NeonResponsesLanguageModel(modelId, {
|
|
162
|
+
provider: "neon.openai.responses",
|
|
163
|
+
url: ({ path }) => `${getHost()}/ai-gateway/openai/v1${path}`,
|
|
164
|
+
headers: getHeaders,
|
|
165
|
+
fetch: options.fetch,
|
|
166
|
+
fileIdPrefixes: ["file-"],
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Everything else (Gemini, Llama, Qwen, gpt-oss, ...) -> unified MLflow
|
|
170
|
+
// endpoint. Gemini is here because its native endpoint can't stream.
|
|
171
|
+
const createChatModel = (modelId: NeonChatModelId) =>
|
|
172
|
+
new NeonChatLanguageModel(modelId, {
|
|
173
|
+
provider: "neon.chat",
|
|
174
|
+
url: ({ path }) => `${getHost()}/ai-gateway/mlflow/v1${path}`,
|
|
175
|
+
headers: getHeaders,
|
|
176
|
+
fetch: options.fetch,
|
|
177
|
+
errorStructure: neonErrorStructure,
|
|
178
|
+
transformRequestBody: transformNeonRequestBody,
|
|
179
|
+
supportsStructuredOutputs: true,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const createLanguageModel = (modelId: NeonChatModelId): LanguageModelV4 => {
|
|
183
|
+
switch (getNeonModelRoute(modelId)) {
|
|
184
|
+
case "anthropic":
|
|
185
|
+
return createAnthropicModel(modelId);
|
|
186
|
+
case "openai":
|
|
187
|
+
return createOpenAIModel(modelId);
|
|
188
|
+
default:
|
|
189
|
+
return createChatModel(modelId);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const provider = (modelId: NeonChatModelId) => createLanguageModel(modelId);
|
|
194
|
+
|
|
195
|
+
provider.specificationVersion = "v4" as const;
|
|
196
|
+
provider.languageModel = createLanguageModel;
|
|
197
|
+
provider.chat = createLanguageModel;
|
|
198
|
+
|
|
199
|
+
provider.embeddingModel = (modelId: string) => {
|
|
200
|
+
throw new NoSuchModelError({ modelId, modelType: "embeddingModel" });
|
|
201
|
+
};
|
|
202
|
+
provider.textEmbeddingModel = provider.embeddingModel;
|
|
203
|
+
provider.imageModel = (modelId: string) => {
|
|
204
|
+
throw new NoSuchModelError({ modelId, modelType: "imageModel" });
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return provider;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Default Neon provider instance (reads `NEON_AI_GATEWAY_*` env vars). */
|
|
211
|
+
export const neon = createNeon();
|
package/src/v1.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@neondatabase/ai-sdk-provider/v1` — community Vercel AI SDK provider for the
|
|
3
|
+
* Neon AI Gateway.
|
|
4
|
+
*
|
|
5
|
+
* - `createNeon()` / `neon` — the provider. Routes each model to the best
|
|
6
|
+
* gateway endpoint (Anthropic → native Messages, OpenAI → native Responses
|
|
7
|
+
* incl. Codex, everything else → unified MLflow).
|
|
8
|
+
*/
|
|
9
|
+
export { NeonAnthropicLanguageModel } from "./lib/neon-anthropic-language-model.js";
|
|
10
|
+
export { NeonChatLanguageModel } from "./lib/neon-chat-language-model.js";
|
|
11
|
+
export type { NeonChatModelId } from "./lib/neon-chat-options.js";
|
|
12
|
+
export {
|
|
13
|
+
getNeonModelCapabilities,
|
|
14
|
+
getNeonModelRoute,
|
|
15
|
+
type NeonModelCapabilities,
|
|
16
|
+
type NeonModelFamily,
|
|
17
|
+
type NeonModelRoute,
|
|
18
|
+
} from "./lib/neon-model-capabilities.js";
|
|
19
|
+
export { NeonResponsesLanguageModel } from "./lib/neon-responses-language-model.js";
|
|
20
|
+
export {
|
|
21
|
+
createNeon,
|
|
22
|
+
type NeonErrorData,
|
|
23
|
+
type NeonProvider,
|
|
24
|
+
type NeonProviderSettings,
|
|
25
|
+
neon,
|
|
26
|
+
} from "./lib/provider.js";
|
package/tsconfig.json
ADDED
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from "tsdown";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
name: "@neondatabase/ai-sdk-provider",
|
|
5
|
+
bundle: false,
|
|
6
|
+
clean: true,
|
|
7
|
+
dts: true,
|
|
8
|
+
entry: ["src/index.ts", "src/v1.ts", "src/lib/**/*.ts", "!src/**/*.test.*"],
|
|
9
|
+
format: "esm",
|
|
10
|
+
outDir: "dist",
|
|
11
|
+
treeshake: true,
|
|
12
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
clearMocks: true,
|
|
6
|
+
mockReset: true,
|
|
7
|
+
unstubEnvs: true,
|
|
8
|
+
coverage: {
|
|
9
|
+
all: true,
|
|
10
|
+
include: ["src"],
|
|
11
|
+
exclude: ["src/**/*.test.ts"],
|
|
12
|
+
reporter: ["html", "lcov"],
|
|
13
|
+
},
|
|
14
|
+
exclude: ["lib", "node_modules", "dist"],
|
|
15
|
+
setupFiles: ["console-fail-test/setup"],
|
|
16
|
+
},
|
|
17
|
+
});
|