@tanstack/ai-cloudflare 0.0.0 → 0.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 +21 -0
- package/README.md +53 -2
- package/dist/esm/adapters/embedding.d.ts +19 -0
- package/dist/esm/adapters/embedding.js +60 -0
- package/dist/esm/adapters/embedding.js.map +1 -0
- package/dist/esm/adapters/image.d.ts +26 -0
- package/dist/esm/adapters/image.js +62 -0
- package/dist/esm/adapters/image.js.map +1 -0
- package/dist/esm/adapters/summarize.d.ts +15 -0
- package/dist/esm/adapters/summarize.js +22 -0
- package/dist/esm/adapters/summarize.js.map +1 -0
- package/dist/esm/adapters/text.d.ts +71 -0
- package/dist/esm/adapters/text.js +88 -0
- package/dist/esm/adapters/text.js.map +1 -0
- package/dist/esm/adapters/transcription.d.ts +21 -0
- package/dist/esm/adapters/transcription.js +134 -0
- package/dist/esm/adapters/transcription.js.map +1 -0
- package/dist/esm/adapters/tts.d.ts +26 -0
- package/dist/esm/adapters/tts.js +68 -0
- package/dist/esm/adapters/tts.js.map +1 -0
- package/dist/esm/byok.d.ts +9 -0
- package/dist/esm/byok.js +24 -0
- package/dist/esm/byok.js.map +1 -0
- package/dist/esm/gateway.d.ts +30 -0
- package/dist/esm/gateway.js +45 -0
- package/dist/esm/gateway.js.map +1 -0
- package/dist/esm/index.d.ts +16 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/utils/config.d.ts +60 -0
- package/dist/esm/utils/config.js +48 -0
- package/dist/esm/utils/config.js.map +1 -0
- package/dist/esm/utils/fetch.d.ts +27 -0
- package/dist/esm/utils/fetch.js +94 -0
- package/dist/esm/utils/fetch.js.map +1 -0
- package/dist/esm/utils/models.d.ts +16 -0
- package/dist/esm/utils/run.d.ts +21 -0
- package/dist/esm/utils/run.js +62 -0
- package/dist/esm/utils/run.js.map +1 -0
- package/package.json +73 -4
- package/src/adapters/embedding.ts +84 -0
- package/src/adapters/image.ts +97 -0
- package/src/adapters/summarize.ts +46 -0
- package/src/adapters/text.ts +150 -0
- package/src/adapters/transcription.ts +227 -0
- package/src/adapters/tts.ts +100 -0
- package/src/byok.ts +21 -0
- package/src/gateway.ts +57 -0
- package/src/index.ts +68 -0
- package/src/utils/config.ts +129 -0
- package/src/utils/fetch.ts +131 -0
- package/src/utils/models.ts +41 -0
- package/src/utils/run.ts +116 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Tanner Linsley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,54 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source
|
|
4
|
+
media="(prefers-color-scheme: dark)"
|
|
5
|
+
srcset="https://tanstack.com/api/readme/ai.png?theme=dark"
|
|
6
|
+
/>
|
|
7
|
+
<source
|
|
8
|
+
media="(prefers-color-scheme: light)"
|
|
9
|
+
srcset="https://tanstack.com/api/readme/ai.png"
|
|
10
|
+
/>
|
|
11
|
+
<img
|
|
12
|
+
src="https://tanstack.com/api/readme/ai.png"
|
|
13
|
+
alt="TanStack AI"
|
|
14
|
+
width="900"
|
|
15
|
+
/>
|
|
16
|
+
</picture>
|
|
17
|
+
</div>
|
|
2
18
|
|
|
3
|
-
|
|
19
|
+
<br />
|
|
20
|
+
|
|
21
|
+
# @tanstack/ai-cloudflare
|
|
22
|
+
|
|
23
|
+
Cloudflare adapter for TanStack AI: Workers AI chat, summarization, embeddings, image generation, text-to-speech, and transcription over the `env.AI` binding or the REST API, with AI Gateway routing for any provider.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @tanstack/ai @tanstack/ai-cloudflare
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
35
|
+
import { createCloudflareText } from '@tanstack/ai-cloudflare'
|
|
36
|
+
import type { Ai } from '@cloudflare/workers-types'
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
async fetch(request: Request, env: { AI: Ai }) {
|
|
40
|
+
const { messages } = await request.json()
|
|
41
|
+
const stream = chat({
|
|
42
|
+
adapter: createCloudflareText('@cf/zai-org/glm-5.3-flash', {
|
|
43
|
+
binding: env.AI,
|
|
44
|
+
}),
|
|
45
|
+
messages,
|
|
46
|
+
})
|
|
47
|
+
return toServerSentEventsResponse(stream)
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Outside a Worker, pass `{ accountId, apiKey }` or set `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` and use `cloudflareText(model)`.
|
|
53
|
+
|
|
54
|
+
See the [docs](https://tanstack.com/ai/latest/docs/adapters/cloudflare) for AI Gateway, media adapters, and model options.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BaseEmbeddingAdapter } from '@tanstack/ai/adapters';
|
|
2
|
+
import { EmbeddingOptions, EmbeddingResult } from '@tanstack/ai';
|
|
3
|
+
import { CloudflareConfig, CloudflareConfigInput } from '../utils/config.js';
|
|
4
|
+
import { CloudflareEmbeddingModel } from '../utils/models.js';
|
|
5
|
+
/** Extra inputs forwarded to the embedding model (model specific). */
|
|
6
|
+
export type CloudflareEmbeddingProviderOptions = Record<string, unknown>;
|
|
7
|
+
/**
|
|
8
|
+
* Cloudflare embedding adapter. Runs Workers AI text-embedding models
|
|
9
|
+
* (`{ text: [...] }` in, `{ data: number[][] }` out) through the binding or
|
|
10
|
+
* the REST API.
|
|
11
|
+
*/
|
|
12
|
+
export declare class CloudflareEmbeddingAdapter<TModel extends CloudflareEmbeddingModel> extends BaseEmbeddingAdapter<TModel, CloudflareEmbeddingProviderOptions> {
|
|
13
|
+
private readonly cfConfig;
|
|
14
|
+
readonly name: "cloudflare";
|
|
15
|
+
constructor(cfConfig: CloudflareConfig, model: TModel);
|
|
16
|
+
createEmbeddings(options: EmbeddingOptions<CloudflareEmbeddingProviderOptions>): Promise<EmbeddingResult>;
|
|
17
|
+
}
|
|
18
|
+
export declare function createCloudflareEmbedding<TModel extends CloudflareEmbeddingModel>(model: TModel, config: CloudflareConfig): CloudflareEmbeddingAdapter<TModel>;
|
|
19
|
+
export declare function cloudflareEmbedding<TModel extends CloudflareEmbeddingModel>(model: TModel, config?: CloudflareConfigInput): CloudflareEmbeddingAdapter<TModel>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { resolveConfigFromEnv } from "../utils/config.js";
|
|
2
|
+
import { runModel } from "../utils/run.js";
|
|
3
|
+
import { generateId } from "@tanstack/ai-utils";
|
|
4
|
+
import { BaseEmbeddingAdapter } from "@tanstack/ai/adapters";
|
|
5
|
+
import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
|
|
6
|
+
import { requireTextOnlyEmbeddingInput } from "@tanstack/ai";
|
|
7
|
+
//#region src/adapters/embedding.ts
|
|
8
|
+
/**
|
|
9
|
+
* Cloudflare embedding adapter. Runs Workers AI text-embedding models
|
|
10
|
+
* (`{ text: [...] }` in, `{ data: number[][] }` out) through the binding or
|
|
11
|
+
* the REST API.
|
|
12
|
+
*/
|
|
13
|
+
var CloudflareEmbeddingAdapter = class extends BaseEmbeddingAdapter {
|
|
14
|
+
cfConfig;
|
|
15
|
+
name = "cloudflare";
|
|
16
|
+
constructor(cfConfig, model) {
|
|
17
|
+
super(model, {});
|
|
18
|
+
this.cfConfig = cfConfig;
|
|
19
|
+
}
|
|
20
|
+
async createEmbeddings(options) {
|
|
21
|
+
const { model, logger } = options;
|
|
22
|
+
const texts = requireTextOnlyEmbeddingInput(options.input, this.name, model);
|
|
23
|
+
if (options.dimensions !== void 0) throw new Error("Workers AI embedding models have fixed dimensions; do not set `dimensions`");
|
|
24
|
+
try {
|
|
25
|
+
logger.request(`activity=embed provider=${this.name} model=${model} inputs=${texts.length}`, {
|
|
26
|
+
provider: this.name,
|
|
27
|
+
model
|
|
28
|
+
});
|
|
29
|
+
const output = await runModel(this.cfConfig, model, {
|
|
30
|
+
...options.modelOptions,
|
|
31
|
+
text: texts
|
|
32
|
+
});
|
|
33
|
+
if (!Array.isArray(output.data) || output.data.length !== texts.length) throw new Error(`Workers AI ${model} returned ${output.data?.length ?? 0} embeddings for ${texts.length} inputs`);
|
|
34
|
+
return {
|
|
35
|
+
id: generateId(this.name),
|
|
36
|
+
model,
|
|
37
|
+
embeddings: output.data.map((vector, index) => ({
|
|
38
|
+
vector,
|
|
39
|
+
index
|
|
40
|
+
}))
|
|
41
|
+
};
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logger.errors(`${this.name}.createEmbeddings fatal`, {
|
|
44
|
+
error: toRunErrorPayload(error, `${this.name}.createEmbeddings failed`),
|
|
45
|
+
source: `${this.name}.createEmbeddings`
|
|
46
|
+
});
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function createCloudflareEmbedding(model, config) {
|
|
52
|
+
return new CloudflareEmbeddingAdapter(config, model);
|
|
53
|
+
}
|
|
54
|
+
function cloudflareEmbedding(model, config) {
|
|
55
|
+
return new CloudflareEmbeddingAdapter(resolveConfigFromEnv(config), model);
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { CloudflareEmbeddingAdapter, cloudflareEmbedding, createCloudflareEmbedding };
|
|
59
|
+
|
|
60
|
+
//# sourceMappingURL=embedding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embedding.js","names":[],"sources":["../../../src/adapters/embedding.ts"],"sourcesContent":["import { BaseEmbeddingAdapter } from '@tanstack/ai/adapters'\nimport { toRunErrorPayload } from '@tanstack/ai/adapter-internals'\nimport { generateId } from '@tanstack/ai-utils'\nimport { requireTextOnlyEmbeddingInput } from '@tanstack/ai'\nimport { resolveConfigFromEnv } from '../utils/config'\nimport { runModel } from '../utils/run'\nimport type { EmbeddingOptions, EmbeddingResult } from '@tanstack/ai'\nimport type { CloudflareConfig, CloudflareConfigInput } from '../utils/config'\nimport type { CloudflareEmbeddingModel } from '../utils/models'\n\n/** Extra inputs forwarded to the embedding model (model specific). */\nexport type CloudflareEmbeddingProviderOptions = Record<string, unknown>\n\n/**\n * Cloudflare embedding adapter. Runs Workers AI text-embedding models\n * (`{ text: [...] }` in, `{ data: number[][] }` out) through the binding or\n * the REST API.\n */\nexport class CloudflareEmbeddingAdapter<\n TModel extends CloudflareEmbeddingModel,\n> extends BaseEmbeddingAdapter<TModel, CloudflareEmbeddingProviderOptions> {\n readonly name = 'cloudflare' as const\n\n constructor(\n private readonly cfConfig: CloudflareConfig,\n model: TModel,\n ) {\n super(model, {})\n }\n\n async createEmbeddings(\n options: EmbeddingOptions<CloudflareEmbeddingProviderOptions>,\n ): Promise<EmbeddingResult> {\n const { model, logger } = options\n const texts = requireTextOnlyEmbeddingInput(options.input, this.name, model)\n if (options.dimensions !== undefined) {\n throw new Error(\n 'Workers AI embedding models have fixed dimensions; do not set `dimensions`',\n )\n }\n try {\n logger.request(\n `activity=embed provider=${this.name} model=${model} inputs=${texts.length}`,\n { provider: this.name, model },\n )\n const output = (await runModel(this.cfConfig, model, {\n ...options.modelOptions,\n text: texts,\n })) as { data?: Array<Array<number>> }\n if (!Array.isArray(output.data) || output.data.length !== texts.length) {\n throw new Error(\n `Workers AI ${model} returned ${output.data?.length ?? 0} embeddings for ${texts.length} inputs`,\n )\n }\n return {\n id: generateId(this.name),\n model,\n embeddings: output.data.map((vector, index) => ({\n vector,\n index,\n })),\n }\n } catch (error: unknown) {\n logger.errors(`${this.name}.createEmbeddings fatal`, {\n error: toRunErrorPayload(error, `${this.name}.createEmbeddings failed`),\n source: `${this.name}.createEmbeddings`,\n })\n throw error\n }\n }\n}\n\nexport function createCloudflareEmbedding<\n TModel extends CloudflareEmbeddingModel,\n>(model: TModel, config: CloudflareConfig): CloudflareEmbeddingAdapter<TModel> {\n return new CloudflareEmbeddingAdapter(config, model)\n}\n\nexport function cloudflareEmbedding<TModel extends CloudflareEmbeddingModel>(\n model: TModel,\n config?: CloudflareConfigInput,\n): CloudflareEmbeddingAdapter<TModel> {\n return new CloudflareEmbeddingAdapter(resolveConfigFromEnv(config), model)\n}\n"],"mappings":";;;;;;;;;;;;AAkBA,IAAa,6BAAb,cAEU,qBAAiE;CAItD;CAHnB,OAAgB;CAEhB,YACE,UACA,OACA;EACA,MAAM,OAAO,CAAC,CAAC;EAHE,KAAA,WAAA;CAInB;CAEA,MAAM,iBACJ,SAC0B;EAC1B,MAAM,EAAE,OAAO,WAAW;EAC1B,MAAM,QAAQ,8BAA8B,QAAQ,OAAO,KAAK,MAAM,KAAK;EAC3E,IAAI,QAAQ,eAAe,KAAA,GACzB,MAAM,IAAI,MACR,4EACF;EAEF,IAAI;GACF,OAAO,QACL,2BAA2B,KAAK,KAAK,SAAS,MAAM,UAAU,MAAM,UACpE;IAAE,UAAU,KAAK;IAAM;GAAM,CAC/B;GACA,MAAM,SAAU,MAAM,SAAS,KAAK,UAAU,OAAO;IACnD,GAAG,QAAQ;IACX,MAAM;GACR,CAAC;GACD,IAAI,CAAC,MAAM,QAAQ,OAAO,IAAI,KAAK,OAAO,KAAK,WAAW,MAAM,QAC9D,MAAM,IAAI,MACR,cAAc,MAAM,YAAY,OAAO,MAAM,UAAU,EAAE,kBAAkB,MAAM,OAAO,QAC1F;GAEF,OAAO;IACL,IAAI,WAAW,KAAK,IAAI;IACxB;IACA,YAAY,OAAO,KAAK,KAAK,QAAQ,WAAW;KAC9C;KACA;IACF,EAAE;GACJ;EACF,SAAS,OAAgB;GACvB,OAAO,OAAO,GAAG,KAAK,KAAK,0BAA0B;IACnD,OAAO,kBAAkB,OAAO,GAAG,KAAK,KAAK,yBAAyB;IACtE,QAAQ,GAAG,KAAK,KAAK;GACvB,CAAC;GACD,MAAM;EACR;CACF;AACF;AAEA,SAAgB,0BAEd,OAAe,QAA8D;CAC7E,OAAO,IAAI,2BAA2B,QAAQ,KAAK;AACrD;AAEA,SAAgB,oBACd,OACA,QACoC;CACpC,OAAO,IAAI,2BAA2B,qBAAqB,MAAM,GAAG,KAAK;AAC3E"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BaseImageAdapter } from '@tanstack/ai/adapters';
|
|
2
|
+
import { ImageGenerationOptions, ImageGenerationResult } from '@tanstack/ai';
|
|
3
|
+
import { CloudflareConfig, CloudflareConfigInput } from '../utils/config.js';
|
|
4
|
+
import { CloudflareImageModel } from '../utils/models.js';
|
|
5
|
+
/** Text-to-image inputs forwarded to the model (`steps`, `guidance`, ...). */
|
|
6
|
+
export interface CloudflareImageProviderOptions {
|
|
7
|
+
negative_prompt?: string;
|
|
8
|
+
steps?: number;
|
|
9
|
+
num_steps?: number;
|
|
10
|
+
guidance?: number;
|
|
11
|
+
seed?: number;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Cloudflare image adapter. Runs Workers AI text-to-image models and returns
|
|
16
|
+
* base64 images, whether the model answers with `{ image }` JSON (Flux,
|
|
17
|
+
* Leonardo) or raw PNG bytes (Stable Diffusion).
|
|
18
|
+
*/
|
|
19
|
+
export declare class CloudflareImageAdapter<TModel extends CloudflareImageModel> extends BaseImageAdapter<TModel, CloudflareImageProviderOptions> {
|
|
20
|
+
private readonly cfConfig;
|
|
21
|
+
readonly name: "cloudflare";
|
|
22
|
+
constructor(cfConfig: CloudflareConfig, model: TModel);
|
|
23
|
+
generateImages(options: ImageGenerationOptions<CloudflareImageProviderOptions>): Promise<ImageGenerationResult>;
|
|
24
|
+
}
|
|
25
|
+
export declare function createCloudflareImage<TModel extends CloudflareImageModel>(model: TModel, config: CloudflareConfig): CloudflareImageAdapter<TModel>;
|
|
26
|
+
export declare function cloudflareImage<TModel extends CloudflareImageModel>(model: TModel, config?: CloudflareConfigInput): CloudflareImageAdapter<TModel>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { resolveConfigFromEnv } from "../utils/config.js";
|
|
2
|
+
import { outputToBase64, runModel } from "../utils/run.js";
|
|
3
|
+
import { generateId } from "@tanstack/ai-utils";
|
|
4
|
+
import { BaseImageAdapter } from "@tanstack/ai/adapters";
|
|
5
|
+
import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
|
|
6
|
+
import { resolveMediaPrompt } from "@tanstack/ai";
|
|
7
|
+
//#region src/adapters/image.ts
|
|
8
|
+
/**
|
|
9
|
+
* Cloudflare image adapter. Runs Workers AI text-to-image models and returns
|
|
10
|
+
* base64 images, whether the model answers with `{ image }` JSON (Flux,
|
|
11
|
+
* Leonardo) or raw PNG bytes (Stable Diffusion).
|
|
12
|
+
*/
|
|
13
|
+
var CloudflareImageAdapter = class extends BaseImageAdapter {
|
|
14
|
+
cfConfig;
|
|
15
|
+
name = "cloudflare";
|
|
16
|
+
constructor(cfConfig, model) {
|
|
17
|
+
super(model, {});
|
|
18
|
+
this.cfConfig = cfConfig;
|
|
19
|
+
}
|
|
20
|
+
async generateImages(options) {
|
|
21
|
+
const { model, logger, numberOfImages = 1 } = options;
|
|
22
|
+
const prompt = resolveMediaPrompt(options.prompt);
|
|
23
|
+
const [width, height] = options.size?.split("x").map(Number) ?? [];
|
|
24
|
+
const inputs = {
|
|
25
|
+
...width && { width },
|
|
26
|
+
...height && { height },
|
|
27
|
+
...options.modelOptions,
|
|
28
|
+
prompt: prompt.text
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
logger.request(`activity=image provider=${this.name} model=${model} n=${numberOfImages}`, {
|
|
32
|
+
provider: this.name,
|
|
33
|
+
model
|
|
34
|
+
});
|
|
35
|
+
const images = await Promise.all(Array.from({ length: numberOfImages }, async () => {
|
|
36
|
+
const output = await runModel(this.cfConfig, model, inputs, { signal: options.abortSignal });
|
|
37
|
+
return { b64Json: output && typeof output === "object" && "image" in output ? output.image : await outputToBase64(output) };
|
|
38
|
+
}));
|
|
39
|
+
return {
|
|
40
|
+
id: generateId(this.name),
|
|
41
|
+
model,
|
|
42
|
+
images
|
|
43
|
+
};
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logger.errors(`${this.name}.generateImages fatal`, {
|
|
46
|
+
error: toRunErrorPayload(error, `${this.name}.generateImages failed`),
|
|
47
|
+
source: `${this.name}.generateImages`
|
|
48
|
+
});
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
function createCloudflareImage(model, config) {
|
|
54
|
+
return new CloudflareImageAdapter(config, model);
|
|
55
|
+
}
|
|
56
|
+
function cloudflareImage(model, config) {
|
|
57
|
+
return new CloudflareImageAdapter(resolveConfigFromEnv(config), model);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { CloudflareImageAdapter, cloudflareImage, createCloudflareImage };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.js","names":[],"sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport { toRunErrorPayload } from '@tanstack/ai/adapter-internals'\nimport { generateId } from '@tanstack/ai-utils'\nimport { resolveMediaPrompt } from '@tanstack/ai'\nimport { resolveConfigFromEnv } from '../utils/config'\nimport { outputToBase64, runModel } from '../utils/run'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n} from '@tanstack/ai'\nimport type { CloudflareConfig, CloudflareConfigInput } from '../utils/config'\nimport type { CloudflareImageModel } from '../utils/models'\n\n/** Text-to-image inputs forwarded to the model (`steps`, `guidance`, ...). */\nexport interface CloudflareImageProviderOptions {\n negative_prompt?: string\n steps?: number\n num_steps?: number\n guidance?: number\n seed?: number\n [key: string]: unknown\n}\n\n/**\n * Cloudflare image adapter. Runs Workers AI text-to-image models and returns\n * base64 images, whether the model answers with `{ image }` JSON (Flux,\n * Leonardo) or raw PNG bytes (Stable Diffusion).\n */\nexport class CloudflareImageAdapter<\n TModel extends CloudflareImageModel,\n> extends BaseImageAdapter<TModel, CloudflareImageProviderOptions> {\n readonly name = 'cloudflare' as const\n\n constructor(\n private readonly cfConfig: CloudflareConfig,\n model: TModel,\n ) {\n super(model, {})\n }\n\n async generateImages(\n options: ImageGenerationOptions<CloudflareImageProviderOptions>,\n ): Promise<ImageGenerationResult> {\n const { model, logger, numberOfImages = 1 } = options\n const prompt = resolveMediaPrompt(options.prompt)\n const [width, height] = options.size?.split('x').map(Number) ?? []\n const inputs = {\n ...(width && { width }),\n ...(height && { height }),\n ...options.modelOptions,\n prompt: prompt.text,\n }\n try {\n logger.request(\n `activity=image provider=${this.name} model=${model} n=${numberOfImages}`,\n { provider: this.name, model },\n )\n const images = await Promise.all(\n Array.from(\n { length: numberOfImages },\n async (): Promise<GeneratedImage> => {\n const output = await runModel(this.cfConfig, model, inputs, {\n signal: options.abortSignal,\n })\n const image =\n output && typeof output === 'object' && 'image' in output\n ? (output as { image: string }).image\n : await outputToBase64(output)\n return { b64Json: image }\n },\n ),\n )\n return { id: generateId(this.name), model, images }\n } catch (error: unknown) {\n logger.errors(`${this.name}.generateImages fatal`, {\n error: toRunErrorPayload(error, `${this.name}.generateImages failed`),\n source: `${this.name}.generateImages`,\n })\n throw error\n }\n }\n}\n\nexport function createCloudflareImage<TModel extends CloudflareImageModel>(\n model: TModel,\n config: CloudflareConfig,\n): CloudflareImageAdapter<TModel> {\n return new CloudflareImageAdapter(config, model)\n}\n\nexport function cloudflareImage<TModel extends CloudflareImageModel>(\n model: TModel,\n config?: CloudflareConfigInput,\n): CloudflareImageAdapter<TModel> {\n return new CloudflareImageAdapter(resolveConfigFromEnv(config), model)\n}\n"],"mappings":";;;;;;;;;;;;AA6BA,IAAa,yBAAb,cAEU,iBAAyD;CAI9C;CAHnB,OAAgB;CAEhB,YACE,UACA,OACA;EACA,MAAM,OAAO,CAAC,CAAC;EAHE,KAAA,WAAA;CAInB;CAEA,MAAM,eACJ,SACgC;EAChC,MAAM,EAAE,OAAO,QAAQ,iBAAiB,MAAM;EAC9C,MAAM,SAAS,mBAAmB,QAAQ,MAAM;EAChD,MAAM,CAAC,OAAO,UAAU,QAAQ,MAAM,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM,KAAK,CAAC;EACjE,MAAM,SAAS;GACb,GAAI,SAAS,EAAE,MAAM;GACrB,GAAI,UAAU,EAAE,OAAO;GACvB,GAAG,QAAQ;GACX,QAAQ,OAAO;EACjB;EACA,IAAI;GACF,OAAO,QACL,2BAA2B,KAAK,KAAK,SAAS,MAAM,KAAK,kBACzD;IAAE,UAAU,KAAK;IAAM;GAAM,CAC/B;GACA,MAAM,SAAS,MAAM,QAAQ,IAC3B,MAAM,KACJ,EAAE,QAAQ,eAAe,GACzB,YAAqC;IACnC,MAAM,SAAS,MAAM,SAAS,KAAK,UAAU,OAAO,QAAQ,EAC1D,QAAQ,QAAQ,YAClB,CAAC;IAKD,OAAO,EAAE,SAHP,UAAU,OAAO,WAAW,YAAY,WAAW,SAC9C,OAA6B,QAC9B,MAAM,eAAe,MAAM,EACT;GAC1B,CACF,CACF;GACA,OAAO;IAAE,IAAI,WAAW,KAAK,IAAI;IAAG;IAAO;GAAO;EACpD,SAAS,OAAgB;GACvB,OAAO,OAAO,GAAG,KAAK,KAAK,wBAAwB;IACjD,OAAO,kBAAkB,OAAO,GAAG,KAAK,KAAK,uBAAuB;IACpE,QAAQ,GAAG,KAAK,KAAK;GACvB,CAAC;GACD,MAAM;EACR;CACF;AACF;AAEA,SAAgB,sBACd,OACA,QACgC;CAChC,OAAO,IAAI,uBAAuB,QAAQ,KAAK;AACjD;AAEA,SAAgB,gBACd,OACA,QACgC;CAChC,OAAO,IAAI,uBAAuB,qBAAqB,MAAM,GAAG,KAAK;AACvE"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ChatStreamSummarizeAdapter, InferTextProviderOptions } from '@tanstack/ai/adapters';
|
|
2
|
+
import { CloudflareTextAdapter } from './text.js';
|
|
3
|
+
import { CloudflareConfigInput, CloudflareTextConfig, CloudflareTextRestConfig } from '../utils/config.js';
|
|
4
|
+
import { CloudflareTextModel } from '../utils/models.js';
|
|
5
|
+
export type CloudflareSummarizeModel = CloudflareTextModel;
|
|
6
|
+
/**
|
|
7
|
+
* Creates a Cloudflare summarize adapter. Summaries run as a chat request
|
|
8
|
+
* against the given model.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createCloudflareSummarize<TModel extends CloudflareSummarizeModel>(model: TModel, config: CloudflareTextConfig): ChatStreamSummarizeAdapter<TModel, InferTextProviderOptions<CloudflareTextAdapter<TModel>>>;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a Cloudflare summarize adapter, reading `CLOUDFLARE_ACCOUNT_ID` and
|
|
13
|
+
* `CLOUDFLARE_API_TOKEN` from the environment unless a binding is passed.
|
|
14
|
+
*/
|
|
15
|
+
export declare function cloudflareSummarize<TModel extends CloudflareSummarizeModel>(model: TModel, config?: CloudflareConfigInput<CloudflareTextRestConfig>): ChatStreamSummarizeAdapter<TModel, InferTextProviderOptions<CloudflareTextAdapter<TModel>>>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolveConfigFromEnv } from "../utils/config.js";
|
|
2
|
+
import { CloudflareTextAdapter } from "./text.js";
|
|
3
|
+
import { ChatStreamSummarizeAdapter } from "@tanstack/ai/adapters";
|
|
4
|
+
//#region src/adapters/summarize.ts
|
|
5
|
+
/**
|
|
6
|
+
* Creates a Cloudflare summarize adapter. Summaries run as a chat request
|
|
7
|
+
* against the given model.
|
|
8
|
+
*/
|
|
9
|
+
function createCloudflareSummarize(model, config) {
|
|
10
|
+
return new ChatStreamSummarizeAdapter(new CloudflareTextAdapter(config, model), model, "cloudflare");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates a Cloudflare summarize adapter, reading `CLOUDFLARE_ACCOUNT_ID` and
|
|
14
|
+
* `CLOUDFLARE_API_TOKEN` from the environment unless a binding is passed.
|
|
15
|
+
*/
|
|
16
|
+
function cloudflareSummarize(model, config) {
|
|
17
|
+
return createCloudflareSummarize(model, resolveConfigFromEnv(config));
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { cloudflareSummarize, createCloudflareSummarize };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=summarize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summarize.js","names":[],"sources":["../../../src/adapters/summarize.ts"],"sourcesContent":["import { ChatStreamSummarizeAdapter } from '@tanstack/ai/adapters'\nimport { resolveConfigFromEnv } from '../utils/config'\nimport { CloudflareTextAdapter } from './text'\nimport type { InferTextProviderOptions } from '@tanstack/ai/adapters'\nimport type {\n CloudflareConfigInput,\n CloudflareTextConfig,\n CloudflareTextRestConfig,\n} from '../utils/config'\nimport type { CloudflareTextModel } from '../utils/models'\n\nexport type CloudflareSummarizeModel = CloudflareTextModel\n\n/**\n * Creates a Cloudflare summarize adapter. Summaries run as a chat request\n * against the given model.\n */\nexport function createCloudflareSummarize<\n TModel extends CloudflareSummarizeModel,\n>(\n model: TModel,\n config: CloudflareTextConfig,\n): ChatStreamSummarizeAdapter<\n TModel,\n InferTextProviderOptions<CloudflareTextAdapter<TModel>>\n> {\n return new ChatStreamSummarizeAdapter(\n new CloudflareTextAdapter(config, model),\n model,\n 'cloudflare',\n )\n}\n\n/**\n * Creates a Cloudflare summarize adapter, reading `CLOUDFLARE_ACCOUNT_ID` and\n * `CLOUDFLARE_API_TOKEN` from the environment unless a binding is passed.\n */\nexport function cloudflareSummarize<TModel extends CloudflareSummarizeModel>(\n model: TModel,\n config?: CloudflareConfigInput<CloudflareTextRestConfig>,\n): ChatStreamSummarizeAdapter<\n TModel,\n InferTextProviderOptions<CloudflareTextAdapter<TModel>>\n> {\n return createCloudflareSummarize(model, resolveConfigFromEnv(config))\n}\n"],"mappings":";;;;;;;;AAiBA,SAAgB,0BAGd,OACA,QAIA;CACA,OAAO,IAAI,2BACT,IAAI,sBAAsB,QAAQ,KAAK,GACvC,OACA,YACF;AACF;;;;;AAMA,SAAgB,oBACd,OACA,QAIA;CACA,OAAO,0BAA0B,OAAO,qBAAqB,MAAM,CAAC;AACtE"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base';
|
|
2
|
+
import { ChatCompletionChunk, ChatCompletionMessageParam } from 'openai/resources/chat/completions/completions';
|
|
3
|
+
import { CloudflareConfigInput, CloudflareTextConfig, CloudflareTextRestConfig } from '../utils/config.js';
|
|
4
|
+
import { CloudflareTextModel } from '../utils/models.js';
|
|
5
|
+
import { ModelMessage } from '@tanstack/ai';
|
|
6
|
+
/**
|
|
7
|
+
* Chat Completions parameters forwarded verbatim to Workers AI. Reasoning
|
|
8
|
+
* models (GLM, Kimi, gpt-oss, QwQ) read `reasoning_effort` and
|
|
9
|
+
* `chat_template_kwargs`; `null` for `reasoning_effort` turns reasoning off.
|
|
10
|
+
*/
|
|
11
|
+
export interface CloudflareTextProviderOptions {
|
|
12
|
+
temperature?: number;
|
|
13
|
+
max_tokens?: number;
|
|
14
|
+
top_p?: number;
|
|
15
|
+
top_k?: number;
|
|
16
|
+
seed?: number;
|
|
17
|
+
repetition_penalty?: number;
|
|
18
|
+
frequency_penalty?: number;
|
|
19
|
+
presence_penalty?: number;
|
|
20
|
+
reasoning_effort?: 'low' | 'medium' | 'high' | null;
|
|
21
|
+
chat_template_kwargs?: {
|
|
22
|
+
enable_thinking?: boolean;
|
|
23
|
+
clear_thinking?: boolean;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Cloudflare text (chat) adapter.
|
|
28
|
+
*
|
|
29
|
+
* Drives Workers AI's OpenAI-compatible Chat Completions surface with the
|
|
30
|
+
* OpenAI SDK. Inside a Worker pass `{ binding: env.AI }`; anywhere else pass
|
|
31
|
+
* `{ accountId, apiKey }`. Add `gateway` to route through AI Gateway. Any
|
|
32
|
+
* catalog model works, including third-party `provider/model` ids billed
|
|
33
|
+
* through AI Gateway.
|
|
34
|
+
*/
|
|
35
|
+
export declare class CloudflareTextAdapter<TModel extends CloudflareTextModel, TProviderOptions extends Record<string, any> = CloudflareTextProviderOptions> extends OpenAIBaseChatCompletionsTextAdapter<TModel, TProviderOptions> {
|
|
36
|
+
readonly kind: "text";
|
|
37
|
+
readonly name: "cloudflare";
|
|
38
|
+
constructor(config: CloudflareTextConfig, model: TModel);
|
|
39
|
+
/**
|
|
40
|
+
* Workers AI validates `messages[].content` as a string, so a tool-call-only
|
|
41
|
+
* assistant turn (which OpenAI accepts as `content: null`) is sent as `''`.
|
|
42
|
+
*/
|
|
43
|
+
protected convertMessage(message: ModelMessage): ChatCompletionMessageParam;
|
|
44
|
+
/**
|
|
45
|
+
* Workers AI accepts `response_format` next to `tools` but its models answer
|
|
46
|
+
* the tool follow-up turn in prose, so structured output with tools runs as
|
|
47
|
+
* a separate finalization request instead.
|
|
48
|
+
*/
|
|
49
|
+
supportsCombinedToolsAndSchema(): boolean;
|
|
50
|
+
/** Workers AI reasoning models stream thinking as `reasoning_content` (some as `reasoning`). */
|
|
51
|
+
protected extractReasoning(chunk: ChatCompletionChunk): {
|
|
52
|
+
text: string;
|
|
53
|
+
} | undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates a Cloudflare text adapter with explicit configuration.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* // Inside a Worker
|
|
61
|
+
* const adapter = createCloudflareText('@cf/zai-org/glm-5.3-flash', { binding: env.AI })
|
|
62
|
+
* // Anywhere, over REST
|
|
63
|
+
* const adapter = createCloudflareText('@cf/zai-org/glm-5.3-flash', { accountId, apiKey })
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function createCloudflareText<TModel extends CloudflareTextModel>(model: TModel, config: CloudflareTextConfig): CloudflareTextAdapter<TModel>;
|
|
67
|
+
/**
|
|
68
|
+
* Creates a Cloudflare text adapter, reading `CLOUDFLARE_ACCOUNT_ID` and
|
|
69
|
+
* `CLOUDFLARE_API_TOKEN` from the environment unless a binding is passed.
|
|
70
|
+
*/
|
|
71
|
+
export declare function cloudflareText<TModel extends CloudflareTextModel>(model: TModel, config?: CloudflareConfigInput<CloudflareTextRestConfig>): CloudflareTextAdapter<TModel>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { gatewayHeaders, isBindingConfig, resolveConfigFromEnv, restChatBaseURL } from "../utils/config.js";
|
|
2
|
+
import { createBindingFetch, createRestFetch } from "../utils/fetch.js";
|
|
3
|
+
import OpenAI from "openai";
|
|
4
|
+
import { OpenAIBaseChatCompletionsTextAdapter } from "@tanstack/openai-base";
|
|
5
|
+
//#region src/adapters/text.ts
|
|
6
|
+
function createClient(config) {
|
|
7
|
+
if (isBindingConfig(config)) return new OpenAI({
|
|
8
|
+
apiKey: "cloudflare-binding",
|
|
9
|
+
fetch: createBindingFetch(config.binding, config.gateway)
|
|
10
|
+
});
|
|
11
|
+
const { accountId: _accountId, binding: _binding, gateway, ...clientOptions } = config;
|
|
12
|
+
return new OpenAI({
|
|
13
|
+
...clientOptions,
|
|
14
|
+
baseURL: restChatBaseURL(config),
|
|
15
|
+
defaultHeaders: {
|
|
16
|
+
...gatewayHeaders(gateway),
|
|
17
|
+
...clientOptions.defaultHeaders
|
|
18
|
+
},
|
|
19
|
+
fetch: createRestFetch(clientOptions.fetch)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Cloudflare text (chat) adapter.
|
|
24
|
+
*
|
|
25
|
+
* Drives Workers AI's OpenAI-compatible Chat Completions surface with the
|
|
26
|
+
* OpenAI SDK. Inside a Worker pass `{ binding: env.AI }`; anywhere else pass
|
|
27
|
+
* `{ accountId, apiKey }`. Add `gateway` to route through AI Gateway. Any
|
|
28
|
+
* catalog model works, including third-party `provider/model` ids billed
|
|
29
|
+
* through AI Gateway.
|
|
30
|
+
*/
|
|
31
|
+
var CloudflareTextAdapter = class extends OpenAIBaseChatCompletionsTextAdapter {
|
|
32
|
+
kind = "text";
|
|
33
|
+
name = "cloudflare";
|
|
34
|
+
constructor(config, model) {
|
|
35
|
+
super(model, "cloudflare", createClient(config));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Workers AI validates `messages[].content` as a string, so a tool-call-only
|
|
39
|
+
* assistant turn (which OpenAI accepts as `content: null`) is sent as `''`.
|
|
40
|
+
*/
|
|
41
|
+
convertMessage(message) {
|
|
42
|
+
const converted = super.convertMessage(message);
|
|
43
|
+
if (converted.role === "assistant" && converted.content == null) return {
|
|
44
|
+
...converted,
|
|
45
|
+
content: ""
|
|
46
|
+
};
|
|
47
|
+
return converted;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Workers AI accepts `response_format` next to `tools` but its models answer
|
|
51
|
+
* the tool follow-up turn in prose, so structured output with tools runs as
|
|
52
|
+
* a separate finalization request instead.
|
|
53
|
+
*/
|
|
54
|
+
supportsCombinedToolsAndSchema() {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/** Workers AI reasoning models stream thinking as `reasoning_content` (some as `reasoning`). */
|
|
58
|
+
extractReasoning(chunk) {
|
|
59
|
+
const delta = chunk.choices[0]?.delta;
|
|
60
|
+
const raw = delta?.reasoning_content ?? delta?.reasoning;
|
|
61
|
+
return typeof raw === "string" && raw.length > 0 ? { text: raw } : void 0;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Creates a Cloudflare text adapter with explicit configuration.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* // Inside a Worker
|
|
70
|
+
* const adapter = createCloudflareText('@cf/zai-org/glm-5.3-flash', { binding: env.AI })
|
|
71
|
+
* // Anywhere, over REST
|
|
72
|
+
* const adapter = createCloudflareText('@cf/zai-org/glm-5.3-flash', { accountId, apiKey })
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
function createCloudflareText(model, config) {
|
|
76
|
+
return new CloudflareTextAdapter(config, model);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Creates a Cloudflare text adapter, reading `CLOUDFLARE_ACCOUNT_ID` and
|
|
80
|
+
* `CLOUDFLARE_API_TOKEN` from the environment unless a binding is passed.
|
|
81
|
+
*/
|
|
82
|
+
function cloudflareText(model, config) {
|
|
83
|
+
return new CloudflareTextAdapter(resolveConfigFromEnv(config), model);
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { CloudflareTextAdapter, cloudflareText, createCloudflareText };
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","names":[],"sources":["../../../src/adapters/text.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'\nimport {\n gatewayHeaders,\n isBindingConfig,\n resolveConfigFromEnv,\n restChatBaseURL,\n} from '../utils/config'\nimport { createBindingFetch, createRestFetch } from '../utils/fetch'\nimport type {\n ChatCompletionChunk,\n ChatCompletionMessageParam,\n} from 'openai/resources/chat/completions/completions'\nimport type {\n CloudflareConfigInput,\n CloudflareTextConfig,\n CloudflareTextRestConfig,\n} from '../utils/config'\nimport type { CloudflareTextModel } from '../utils/models'\nimport type { ModelMessage } from '@tanstack/ai'\n\n/**\n * Chat Completions parameters forwarded verbatim to Workers AI. Reasoning\n * models (GLM, Kimi, gpt-oss, QwQ) read `reasoning_effort` and\n * `chat_template_kwargs`; `null` for `reasoning_effort` turns reasoning off.\n */\nexport interface CloudflareTextProviderOptions {\n temperature?: number\n max_tokens?: number\n top_p?: number\n top_k?: number\n seed?: number\n repetition_penalty?: number\n frequency_penalty?: number\n presence_penalty?: number\n reasoning_effort?: 'low' | 'medium' | 'high' | null\n chat_template_kwargs?: {\n enable_thinking?: boolean\n clear_thinking?: boolean\n }\n}\n\nfunction createClient(config: CloudflareTextConfig): OpenAI {\n if (isBindingConfig(config)) {\n return new OpenAI({\n // The binding authenticates by itself; the SDK only requires a value.\n apiKey: 'cloudflare-binding',\n fetch: createBindingFetch(config.binding, config.gateway),\n })\n }\n const {\n accountId: _accountId,\n binding: _binding,\n gateway,\n ...clientOptions\n } = config\n return new OpenAI({\n ...clientOptions,\n baseURL: restChatBaseURL(config),\n defaultHeaders: {\n ...gatewayHeaders(gateway),\n ...clientOptions.defaultHeaders,\n },\n fetch: createRestFetch(clientOptions.fetch),\n })\n}\n\n/**\n * Cloudflare text (chat) adapter.\n *\n * Drives Workers AI's OpenAI-compatible Chat Completions surface with the\n * OpenAI SDK. Inside a Worker pass `{ binding: env.AI }`; anywhere else pass\n * `{ accountId, apiKey }`. Add `gateway` to route through AI Gateway. Any\n * catalog model works, including third-party `provider/model` ids billed\n * through AI Gateway.\n */\nexport class CloudflareTextAdapter<\n TModel extends CloudflareTextModel,\n TProviderOptions extends Record<string, any> = CloudflareTextProviderOptions,\n> extends OpenAIBaseChatCompletionsTextAdapter<TModel, TProviderOptions> {\n override readonly kind = 'text' as const\n override readonly name = 'cloudflare' as const\n\n constructor(config: CloudflareTextConfig, model: TModel) {\n super(model, 'cloudflare', createClient(config))\n }\n\n /**\n * Workers AI validates `messages[].content` as a string, so a tool-call-only\n * assistant turn (which OpenAI accepts as `content: null`) is sent as `''`.\n */\n protected override convertMessage(\n message: ModelMessage,\n ): ChatCompletionMessageParam {\n const converted = super.convertMessage(message)\n if (converted.role === 'assistant' && converted.content == null) {\n return { ...converted, content: '' }\n }\n return converted\n }\n\n /**\n * Workers AI accepts `response_format` next to `tools` but its models answer\n * the tool follow-up turn in prose, so structured output with tools runs as\n * a separate finalization request instead.\n */\n override supportsCombinedToolsAndSchema(): boolean {\n return false\n }\n\n /** Workers AI reasoning models stream thinking as `reasoning_content` (some as `reasoning`). */\n protected override extractReasoning(\n chunk: ChatCompletionChunk,\n ): { text: string } | undefined {\n const delta = chunk.choices[0]?.delta as\n | { reasoning?: unknown; reasoning_content?: unknown }\n | undefined\n const raw = delta?.reasoning_content ?? delta?.reasoning\n return typeof raw === 'string' && raw.length > 0 ? { text: raw } : undefined\n }\n}\n\n/**\n * Creates a Cloudflare text adapter with explicit configuration.\n *\n * @example\n * ```typescript\n * // Inside a Worker\n * const adapter = createCloudflareText('@cf/zai-org/glm-5.3-flash', { binding: env.AI })\n * // Anywhere, over REST\n * const adapter = createCloudflareText('@cf/zai-org/glm-5.3-flash', { accountId, apiKey })\n * ```\n */\nexport function createCloudflareText<TModel extends CloudflareTextModel>(\n model: TModel,\n config: CloudflareTextConfig,\n): CloudflareTextAdapter<TModel> {\n return new CloudflareTextAdapter(config, model)\n}\n\n/**\n * Creates a Cloudflare text adapter, reading `CLOUDFLARE_ACCOUNT_ID` and\n * `CLOUDFLARE_API_TOKEN` from the environment unless a binding is passed.\n */\nexport function cloudflareText<TModel extends CloudflareTextModel>(\n model: TModel,\n config?: CloudflareConfigInput<CloudflareTextRestConfig>,\n): CloudflareTextAdapter<TModel> {\n return new CloudflareTextAdapter(resolveConfigFromEnv(config), model)\n}\n"],"mappings":";;;;;AA0CA,SAAS,aAAa,QAAsC;CAC1D,IAAI,gBAAgB,MAAM,GACxB,OAAO,IAAI,OAAO;EAEhB,QAAQ;EACR,OAAO,mBAAmB,OAAO,SAAS,OAAO,OAAO;CAC1D,CAAC;CAEH,MAAM,EACJ,WAAW,YACX,SAAS,UACT,SACA,GAAG,kBACD;CACJ,OAAO,IAAI,OAAO;EAChB,GAAG;EACH,SAAS,gBAAgB,MAAM;EAC/B,gBAAgB;GACd,GAAG,eAAe,OAAO;GACzB,GAAG,cAAc;EACnB;EACA,OAAO,gBAAgB,cAAc,KAAK;CAC5C,CAAC;AACH;;;;;;;;;;AAWA,IAAa,wBAAb,cAGU,qCAA+D;CACvE,OAAyB;CACzB,OAAyB;CAEzB,YAAY,QAA8B,OAAe;EACvD,MAAM,OAAO,cAAc,aAAa,MAAM,CAAC;CACjD;;;;;CAMA,eACE,SAC4B;EAC5B,MAAM,YAAY,MAAM,eAAe,OAAO;EAC9C,IAAI,UAAU,SAAS,eAAe,UAAU,WAAW,MACzD,OAAO;GAAE,GAAG;GAAW,SAAS;EAAG;EAErC,OAAO;CACT;;;;;;CAOA,iCAAmD;EACjD,OAAO;CACT;;CAGA,iBACE,OAC8B;EAC9B,MAAM,QAAQ,MAAM,QAAQ,EAAE,EAAE;EAGhC,MAAM,MAAM,OAAO,qBAAqB,OAAO;EAC/C,OAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAI,EAAE,MAAM,IAAI,IAAI,KAAA;CACrE;AACF;;;;;;;;;;;;AAaA,SAAgB,qBACd,OACA,QAC+B;CAC/B,OAAO,IAAI,sBAAsB,QAAQ,KAAK;AAChD;;;;;AAMA,SAAgB,eACd,OACA,QAC+B;CAC/B,OAAO,IAAI,sBAAsB,qBAAqB,MAAM,GAAG,KAAK;AACtE"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BaseTranscriptionAdapter } from '@tanstack/ai/adapters';
|
|
2
|
+
import { TranscriptionOptions, TranscriptionResult } from '@tanstack/ai';
|
|
3
|
+
import { CloudflareConfig, CloudflareConfigInput } from '../utils/config.js';
|
|
4
|
+
import { CloudflareTranscriptionModel } from '../utils/models.js';
|
|
5
|
+
/** Extra inputs forwarded to the transcription model (model specific). */
|
|
6
|
+
export type CloudflareTranscriptionProviderOptions = Record<string, unknown>;
|
|
7
|
+
/**
|
|
8
|
+
* Cloudflare transcription adapter. Whisper models take base64 audio in the
|
|
9
|
+
* `audio` input; Deepgram Nova takes the raw bytes. Both return text plus
|
|
10
|
+
* timed words, and Whisper also returns segments.
|
|
11
|
+
*/
|
|
12
|
+
export declare class CloudflareTranscriptionAdapter<TModel extends CloudflareTranscriptionModel> extends BaseTranscriptionAdapter<TModel, CloudflareTranscriptionProviderOptions> {
|
|
13
|
+
private readonly cfConfig;
|
|
14
|
+
readonly name: "cloudflare";
|
|
15
|
+
constructor(cfConfig: CloudflareConfig, model: TModel);
|
|
16
|
+
transcribe(options: TranscriptionOptions<CloudflareTranscriptionProviderOptions>): Promise<TranscriptionResult>;
|
|
17
|
+
private runWhisper;
|
|
18
|
+
private runNova;
|
|
19
|
+
}
|
|
20
|
+
export declare function createCloudflareTranscription<TModel extends CloudflareTranscriptionModel>(model: TModel, config: CloudflareConfig): CloudflareTranscriptionAdapter<TModel>;
|
|
21
|
+
export declare function cloudflareTranscription<TModel extends CloudflareTranscriptionModel>(model: TModel, config?: CloudflareConfigInput): CloudflareTranscriptionAdapter<TModel>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { isBindingConfig, resolveConfigFromEnv } from "../utils/config.js";
|
|
2
|
+
import { runModel } from "../utils/run.js";
|
|
3
|
+
import { arrayBufferToBase64, generateId } from "@tanstack/ai-utils";
|
|
4
|
+
import { BaseTranscriptionAdapter } from "@tanstack/ai/adapters";
|
|
5
|
+
import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
|
|
6
|
+
//#region src/adapters/transcription.ts
|
|
7
|
+
async function toBytes(audio, fetchImpl, signal) {
|
|
8
|
+
if (typeof audio === "string") {
|
|
9
|
+
if (/^https?:\/\//.test(audio)) {
|
|
10
|
+
const response = await fetchImpl(audio, { signal });
|
|
11
|
+
if (!response.ok) throw new Error(`Could not fetch audio from ${audio} (${response.status})`);
|
|
12
|
+
return {
|
|
13
|
+
bytes: await response.arrayBuffer(),
|
|
14
|
+
contentType: response.headers.get("content-type") ?? "audio/mpeg"
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const match = /^data:([^;]+);base64,(.*)$/.exec(audio);
|
|
18
|
+
const base64 = match?.[2] ?? audio;
|
|
19
|
+
const binary = atob(base64);
|
|
20
|
+
const bytes = new Uint8Array(binary.length);
|
|
21
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
22
|
+
return {
|
|
23
|
+
bytes: bytes.buffer,
|
|
24
|
+
contentType: match?.[1] ?? "audio/mpeg"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (audio instanceof ArrayBuffer) return {
|
|
28
|
+
bytes: audio,
|
|
29
|
+
contentType: "audio/mpeg"
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
bytes: await audio.arrayBuffer(),
|
|
33
|
+
contentType: audio.type || "audio/mpeg"
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Cloudflare transcription adapter. Whisper models take base64 audio in the
|
|
38
|
+
* `audio` input; Deepgram Nova takes the raw bytes. Both return text plus
|
|
39
|
+
* timed words, and Whisper also returns segments.
|
|
40
|
+
*/
|
|
41
|
+
var CloudflareTranscriptionAdapter = class extends BaseTranscriptionAdapter {
|
|
42
|
+
cfConfig;
|
|
43
|
+
name = "cloudflare";
|
|
44
|
+
constructor(cfConfig, model) {
|
|
45
|
+
super(model, {});
|
|
46
|
+
this.cfConfig = cfConfig;
|
|
47
|
+
}
|
|
48
|
+
async transcribe(options) {
|
|
49
|
+
const { model, logger, language, prompt } = options;
|
|
50
|
+
try {
|
|
51
|
+
logger.request(`activity=transcription provider=${this.name} model=${model}`, {
|
|
52
|
+
provider: this.name,
|
|
53
|
+
model
|
|
54
|
+
});
|
|
55
|
+
const fetchImpl = (isBindingConfig(this.cfConfig) ? void 0 : this.cfConfig.fetch) ?? fetch;
|
|
56
|
+
const { bytes, contentType } = await toBytes(options.audio, fetchImpl, options.abortSignal);
|
|
57
|
+
const output = model.startsWith("@cf/deepgram/") ? await this.runNova(model, bytes, contentType, options) : await this.runWhisper(model, bytes, {
|
|
58
|
+
language,
|
|
59
|
+
prompt,
|
|
60
|
+
...options.modelOptions
|
|
61
|
+
}, options.abortSignal);
|
|
62
|
+
return {
|
|
63
|
+
id: generateId(this.name),
|
|
64
|
+
model,
|
|
65
|
+
...output
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.errors(`${this.name}.transcribe fatal`, {
|
|
69
|
+
error: toRunErrorPayload(error, `${this.name}.transcribe failed`),
|
|
70
|
+
source: `${this.name}.transcribe`
|
|
71
|
+
});
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async runWhisper(model, bytes, inputs, signal) {
|
|
76
|
+
const { language, prompt, ...rest } = inputs;
|
|
77
|
+
const output = await runModel(this.cfConfig, model, {
|
|
78
|
+
...language && { language },
|
|
79
|
+
...prompt && { initial_prompt: prompt },
|
|
80
|
+
...rest,
|
|
81
|
+
audio: arrayBufferToBase64(bytes)
|
|
82
|
+
}, { signal });
|
|
83
|
+
if (typeof output.text !== "string") throw new Error(`Workers AI ${model} returned no transcript`);
|
|
84
|
+
return {
|
|
85
|
+
text: output.text,
|
|
86
|
+
language: output.transcription_info?.language,
|
|
87
|
+
duration: output.transcription_info?.duration,
|
|
88
|
+
segments: output.segments?.map((segment, id) => ({
|
|
89
|
+
id,
|
|
90
|
+
start: segment.start,
|
|
91
|
+
end: segment.end,
|
|
92
|
+
text: segment.text.trim()
|
|
93
|
+
})),
|
|
94
|
+
words: toWords(output.words)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async runNova(model, bytes, contentType, options) {
|
|
98
|
+
const output = await runModel(this.cfConfig, model, {
|
|
99
|
+
...options.language && { language: options.language },
|
|
100
|
+
...options.modelOptions
|
|
101
|
+
}, {
|
|
102
|
+
signal: options.abortSignal,
|
|
103
|
+
binary: {
|
|
104
|
+
field: "audio",
|
|
105
|
+
body: bytes,
|
|
106
|
+
contentType
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const alternative = output.results?.channels?.[0]?.alternatives?.[0];
|
|
110
|
+
if (typeof alternative?.transcript !== "string") throw new Error(`Workers AI ${model} returned no transcript`);
|
|
111
|
+
return {
|
|
112
|
+
text: alternative.transcript,
|
|
113
|
+
duration: output.metadata?.duration,
|
|
114
|
+
words: toWords(alternative?.words)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function toWords(words) {
|
|
119
|
+
return words?.map((w) => ({
|
|
120
|
+
word: (w.word ?? "").trim(),
|
|
121
|
+
start: w.start ?? 0,
|
|
122
|
+
end: w.end ?? 0
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
function createCloudflareTranscription(model, config) {
|
|
126
|
+
return new CloudflareTranscriptionAdapter(config, model);
|
|
127
|
+
}
|
|
128
|
+
function cloudflareTranscription(model, config) {
|
|
129
|
+
return new CloudflareTranscriptionAdapter(resolveConfigFromEnv(config), model);
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
export { CloudflareTranscriptionAdapter, cloudflareTranscription, createCloudflareTranscription };
|
|
133
|
+
|
|
134
|
+
//# sourceMappingURL=transcription.js.map
|