chore: generate
This commit is contained in:
@@ -90,7 +90,10 @@ export const layer = (options: { readonly directory?: string } = {}) =>
|
|||||||
return (yield* walk(directory))
|
return (yield* walk(directory))
|
||||||
.filter((file) => file.endsWith(".json"))
|
.filter((file) => file.endsWith(".json"))
|
||||||
.map((file) => ({
|
.map((file) => ({
|
||||||
name: path.relative(directory, file).replace(/\\/g, "/").replace(/\.json$/, ""),
|
name: path
|
||||||
|
.relative(directory, file)
|
||||||
|
.replace(/\\/g, "/")
|
||||||
|
.replace(/\.json$/, ""),
|
||||||
path: file,
|
path: file,
|
||||||
}))
|
}))
|
||||||
.toSorted((a, b) => a.name.localeCompare(b.name))
|
.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ const redactionSet = (values: ReadonlyArray<string> | undefined, defaults: Reado
|
|||||||
|
|
||||||
export type UrlRedactor = (url: string) => string
|
export type UrlRedactor = (url: string) => string
|
||||||
|
|
||||||
export const redactUrl = (raw: string, query: ReadonlyArray<string> = DEFAULT_REDACT_QUERY, urlRedactor?: UrlRedactor) => {
|
export const redactUrl = (
|
||||||
|
raw: string,
|
||||||
|
query: ReadonlyArray<string> = DEFAULT_REDACT_QUERY,
|
||||||
|
urlRedactor?: UrlRedactor,
|
||||||
|
) => {
|
||||||
if (!URL.canParse(raw)) return urlRedactor?.(raw) ?? raw
|
if (!URL.canParse(raw)) return urlRedactor?.(raw) ?? raw
|
||||||
const url = new URL(raw)
|
const url = new URL(raw)
|
||||||
if (url.username) url.username = REDACTED
|
if (url.username) url.username = REDACTED
|
||||||
|
|||||||
@@ -64,10 +64,8 @@ describe("http-recorder", () => {
|
|||||||
|
|
||||||
test("applies custom URL redaction after built-in redaction", () => {
|
test("applies custom URL redaction after built-in redaction", () => {
|
||||||
expect(
|
expect(
|
||||||
HttpRecorder.redactUrl(
|
HttpRecorder.redactUrl("https://example.test/accounts/real-account/path?key=secret-key", undefined, (url) =>
|
||||||
"https://example.test/accounts/real-account/path?key=secret-key",
|
url.replace("/accounts/real-account/", "/accounts/{account}/"),
|
||||||
undefined,
|
|
||||||
(url) => url.replace("/accounts/real-account/", "/accounts/{account}/"),
|
|
||||||
),
|
),
|
||||||
).toBe("https://example.test/accounts/{account}/path?key=%5BREDACTED%5D")
|
).toBe("https://example.test/accounts/{account}/path?key=%5BREDACTED%5D")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const request = LLM.request({
|
|||||||
prompt: "Say hello.",
|
prompt: "Say hello.",
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = yield* LLMClient.generate(request)
|
const response = yield * LLMClient.generate(request)
|
||||||
```
|
```
|
||||||
|
|
||||||
`LLM.request(...)` builds an `LLMRequest`. `LLMClient.generate(...)` selects a registered route by `request.model.route`, builds the provider-native body, asks the route's transport for a real `HttpClientRequest.HttpClientRequest`, sends it through `RequestExecutor.Service`, parses the provider stream into common `LLMEvent`s, and finally returns an `LLMResponse`.
|
`LLM.request(...)` builds an `LLMRequest`. `LLMClient.generate(...)` selects a registered route by `request.model.route`, builds the provider-native body, asks the route's transport for a real `HttpClientRequest.HttpClientRequest`, sends it through `RequestExecutor.Service`, parses the provider stream into common `LLMEvent`s, and finally returns an `LLMResponse`.
|
||||||
|
|||||||
@@ -110,7 +110,12 @@ const PROVIDERS: ReadonlyArray<Provider> = [
|
|||||||
note: "Cloudflare Unified/OpenAI-compatible gateway; supports provider/model ids like workers-ai/@cf/...",
|
note: "Cloudflare Unified/OpenAI-compatible gateway; supports provider/model ids like workers-ai/@cf/...",
|
||||||
vars: [
|
vars: [
|
||||||
{ name: "CLOUDFLARE_ACCOUNT_ID", label: "Cloudflare account ID", secret: false },
|
{ name: "CLOUDFLARE_ACCOUNT_ID", label: "Cloudflare account ID", secret: false },
|
||||||
{ name: "CLOUDFLARE_GATEWAY_ID", label: "Cloudflare AI Gateway ID (defaults to default)", optional: true, secret: false },
|
{
|
||||||
|
name: "CLOUDFLARE_GATEWAY_ID",
|
||||||
|
label: "Cloudflare AI Gateway ID (defaults to default)",
|
||||||
|
optional: true,
|
||||||
|
secret: false,
|
||||||
|
},
|
||||||
{ name: "CLOUDFLARE_API_TOKEN", label: "Cloudflare AI Gateway token" },
|
{ name: "CLOUDFLARE_API_TOKEN", label: "Cloudflare AI Gateway token" },
|
||||||
],
|
],
|
||||||
validate: (env) =>
|
validate: (env) =>
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import { Effect, JsonSchema, Schema } from "effect"
|
import { Effect, JsonSchema, Schema } from "effect"
|
||||||
import {
|
import { LLMClient, modelLimits, modelRef, type ModelRefInput } from "./route/client"
|
||||||
LLMClient,
|
|
||||||
modelLimits,
|
|
||||||
modelRef,
|
|
||||||
type ModelRefInput,
|
|
||||||
} from "./route/client"
|
|
||||||
import {
|
import {
|
||||||
GenerationOptions,
|
GenerationOptions,
|
||||||
HttpOptions,
|
HttpOptions,
|
||||||
@@ -196,10 +191,10 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
|
|||||||
export function generateObject<S extends ToolSchema<any>>(
|
export function generateObject<S extends ToolSchema<any>>(
|
||||||
options: GenerateObjectOptions<S>,
|
options: GenerateObjectOptions<S>,
|
||||||
): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, LLMError>
|
): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, LLMError>
|
||||||
export function generateObject(options: GenerateObjectDynamicOptions): Effect.Effect<GenerateObjectResponse<unknown>, LLMError>
|
|
||||||
export function generateObject(
|
export function generateObject(
|
||||||
options: GenerateObjectOptions<ToolSchema<any>> | GenerateObjectDynamicOptions,
|
options: GenerateObjectDynamicOptions,
|
||||||
) {
|
): Effect.Effect<GenerateObjectResponse<unknown>, LLMError>
|
||||||
|
export function generateObject(options: GenerateObjectOptions<ToolSchema<any>> | GenerateObjectDynamicOptions) {
|
||||||
if ("schema" in options) {
|
if ("schema" in options) {
|
||||||
const { schema, ...rest } = options
|
const { schema, ...rest } = options
|
||||||
return runGenerateObject(
|
return runGenerateObject(
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ export const id = ProviderID.make("anthropic")
|
|||||||
|
|
||||||
export const routes = [AnthropicMessages.route]
|
export const routes = [AnthropicMessages.route]
|
||||||
|
|
||||||
export const model = (id: string | ModelID, options: Omit<RouteModelInput, "id" | "baseURL"> & { readonly baseURL?: string } = {}) =>
|
export const model = (
|
||||||
AnthropicMessages.model({ ...options, id })
|
id: string | ModelID,
|
||||||
|
options: Omit<RouteModelInput, "id" | "baseURL"> & { readonly baseURL?: string } = {},
|
||||||
|
) => AnthropicMessages.model({ ...options, id })
|
||||||
|
|
||||||
export const provider = Provider.make({
|
export const provider = Provider.make({
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ export const id = ProviderID.make("google")
|
|||||||
|
|
||||||
export const routes = [Gemini.route]
|
export const routes = [Gemini.route]
|
||||||
|
|
||||||
export const model = (id: string | ModelID, options: Omit<RouteModelInput, "id" | "baseURL"> & { readonly baseURL?: string } = {}) =>
|
export const model = (
|
||||||
Gemini.model({ ...options, id })
|
id: string | ModelID,
|
||||||
|
options: Omit<RouteModelInput, "id" | "baseURL"> & { readonly baseURL?: string } = {},
|
||||||
|
) => Gemini.model({ ...options, id })
|
||||||
|
|
||||||
export const provider = Provider.make({
|
export const provider = Provider.make({
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -168,8 +168,9 @@ export function bearerHeader(
|
|||||||
name: string,
|
name: string,
|
||||||
source?: string | Redacted.Redacted<string> | Config.Config<string | Redacted.Redacted<string>> | Credential,
|
source?: string | Redacted.Redacted<string> | Config.Config<string | Redacted.Redacted<string>> | Credential,
|
||||||
) {
|
) {
|
||||||
const render = (input: string | Redacted.Redacted<string> | Config.Config<string | Redacted.Redacted<string>> | Credential) =>
|
const render = (
|
||||||
fromCredential(credentialInput(input), (secret) => ({ [name]: `Bearer ${secret}` }))
|
input: string | Redacted.Redacted<string> | Config.Config<string | Redacted.Redacted<string>> | Credential,
|
||||||
|
) => fromCredential(credentialInput(input), (secret) => ({ [name]: `Bearer ${secret}` }))
|
||||||
if (source === undefined) return render
|
if (source === undefined) return render
|
||||||
return render(source)
|
return render(source)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,9 +142,7 @@ const modelWithDefaults =
|
|||||||
if (!provider) throw new Error(`Route.model(${route.id}) requires a provider`)
|
if (!provider) throw new Error(`Route.model(${route.id}) requires a provider`)
|
||||||
const baseURL = mapped.baseURL ?? defaults.baseURL ?? route.defaults.baseURL
|
const baseURL = mapped.baseURL ?? defaults.baseURL ?? route.defaults.baseURL
|
||||||
if (!baseURL)
|
if (!baseURL)
|
||||||
throw new Error(
|
throw new Error(`Route.model(${route.id}) requires a baseURL — supply it via input, defaults, or route defaults`)
|
||||||
`Route.model(${route.id}) requires a baseURL — supply it via input, defaults, or route defaults`,
|
|
||||||
)
|
|
||||||
const generation = mergeGenerationOptions(route.defaults.generation, defaults.generation)
|
const generation = mergeGenerationOptions(route.defaults.generation, defaults.generation)
|
||||||
const providerOptions = mergeProviderOptions(route.defaults.providerOptions, defaults.providerOptions)
|
const providerOptions = mergeProviderOptions(route.defaults.providerOptions, defaults.providerOptions)
|
||||||
const http = mergeHttpOptions(httpOptions(route.defaults.http), httpOptions(defaults.http))
|
const http = mergeHttpOptions(httpOptions(route.defaults.http), httpOptions(defaults.http))
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ const renderPart = <Body>(part: EndpointPart<Body>, input: EndpointInput<Body>)
|
|||||||
typeof part === "function" ? part(input) : part
|
typeof part === "function" ? part(input) : part
|
||||||
|
|
||||||
export const render = <Body>(endpoint: Endpoint<Body>, input: EndpointInput<Body>) => {
|
export const render = <Body>(endpoint: Endpoint<Body>, input: EndpointInput<Body>) => {
|
||||||
const url = new URL(
|
const url = new URL(`${ProviderShared.trimBaseUrl(input.request.model.baseURL)}${renderPart(endpoint.path, input)}`)
|
||||||
`${ProviderShared.trimBaseUrl(input.request.model.baseURL)}${renderPart(endpoint.path, input)}`,
|
|
||||||
)
|
|
||||||
const params = input.request.model.queryParams
|
const params = input.request.model.queryParams
|
||||||
if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value)
|
if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value)
|
||||||
return url
|
return url
|
||||||
|
|||||||
@@ -2,12 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import { LLM } from "../src"
|
import { LLM } from "../src"
|
||||||
import { Endpoint } from "../src/route"
|
import { Endpoint } from "../src/route"
|
||||||
|
|
||||||
const request = (
|
const request = (input: { readonly baseURL: string; readonly queryParams?: Record<string, string> }) =>
|
||||||
input: {
|
|
||||||
readonly baseURL: string
|
|
||||||
readonly queryParams?: Record<string, string>
|
|
||||||
},
|
|
||||||
) =>
|
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model: LLM.model({
|
model: LLM.model({
|
||||||
id: "model-1",
|
id: "model-1",
|
||||||
@@ -43,7 +38,9 @@ describe("Endpoint", () => {
|
|||||||
|
|
||||||
test("path may be a function of the validated body", () => {
|
test("path may be a function of the validated body", () => {
|
||||||
const url = Endpoint.render(
|
const url = Endpoint.render(
|
||||||
Endpoint.path<{ readonly modelId: string }>(({ body }) => `/model/${encodeURIComponent(body.modelId)}/converse-stream`),
|
Endpoint.path<{ readonly modelId: string }>(
|
||||||
|
({ body }) => `/model/${encodeURIComponent(body.modelId)}/converse-stream`,
|
||||||
|
),
|
||||||
{
|
{
|
||||||
request: request({ baseURL: "https://bedrock-runtime.us-east-1.amazonaws.com" }),
|
request: request({ baseURL: "https://bedrock-runtime.us-east-1.amazonaws.com" }),
|
||||||
body: { modelId: "us.amazon.nova-micro-v1:0" },
|
body: { modelId: "us.amazon.nova-micro-v1:0" },
|
||||||
|
|||||||
+1
-7
@@ -7,13 +7,7 @@
|
|||||||
"route": "cloudflare-ai-gateway",
|
"route": "cloudflare-ai-gateway",
|
||||||
"transport": "http",
|
"transport": "http",
|
||||||
"model": "workers-ai/@cf/openai/gpt-oss-20b",
|
"model": "workers-ai/@cf/openai/gpt-oss-20b",
|
||||||
"tags": [
|
"tags": ["prefix:cloudflare-ai-gateway", "provider:cloudflare-ai-gateway", "tool", "tool-call", "golden"]
|
||||||
"prefix:cloudflare-ai-gateway",
|
|
||||||
"provider:cloudflare-ai-gateway",
|
|
||||||
"tool",
|
|
||||||
"tool-call",
|
|
||||||
"golden"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"interactions": [
|
"interactions": [
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-6
@@ -7,12 +7,7 @@
|
|||||||
"route": "cloudflare-ai-gateway",
|
"route": "cloudflare-ai-gateway",
|
||||||
"transport": "http",
|
"transport": "http",
|
||||||
"model": "workers-ai/@cf/meta/llama-3.1-8b-instruct",
|
"model": "workers-ai/@cf/meta/llama-3.1-8b-instruct",
|
||||||
"tags": [
|
"tags": ["prefix:cloudflare-ai-gateway", "provider:cloudflare-ai-gateway", "text", "golden"]
|
||||||
"prefix:cloudflare-ai-gateway",
|
|
||||||
"provider:cloudflare-ai-gateway",
|
|
||||||
"text",
|
|
||||||
"golden"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"interactions": [
|
"interactions": [
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-7
@@ -7,13 +7,7 @@
|
|||||||
"route": "cloudflare-workers-ai",
|
"route": "cloudflare-workers-ai",
|
||||||
"transport": "http",
|
"transport": "http",
|
||||||
"model": "@cf/openai/gpt-oss-20b",
|
"model": "@cf/openai/gpt-oss-20b",
|
||||||
"tags": [
|
"tags": ["prefix:cloudflare-workers-ai", "provider:cloudflare-workers-ai", "tool", "tool-call", "golden"]
|
||||||
"prefix:cloudflare-workers-ai",
|
|
||||||
"provider:cloudflare-workers-ai",
|
|
||||||
"tool",
|
|
||||||
"tool-call",
|
|
||||||
"golden"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"interactions": [
|
"interactions": [
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-6
@@ -7,12 +7,7 @@
|
|||||||
"route": "cloudflare-workers-ai",
|
"route": "cloudflare-workers-ai",
|
||||||
"transport": "http",
|
"transport": "http",
|
||||||
"model": "@cf/meta/llama-3.1-8b-instruct",
|
"model": "@cf/meta/llama-3.1-8b-instruct",
|
||||||
"tags": [
|
"tags": ["prefix:cloudflare-workers-ai", "provider:cloudflare-workers-ai", "text", "golden"]
|
||||||
"prefix:cloudflare-workers-ai",
|
|
||||||
"provider:cloudflare-workers-ai",
|
|
||||||
"text",
|
|
||||||
"golden"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"interactions": [
|
"interactions": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -164,7 +164,10 @@ describe("LLM.generateObject", () => {
|
|||||||
const layer = dynamicResponse((input) =>
|
const layer = dynamicResponse((input) =>
|
||||||
Effect.sync(() =>
|
Effect.sync(() =>
|
||||||
input.respond(
|
input.respond(
|
||||||
sseEvents(toolCallChunk("call_1", "generate_object", '{"value":"not-a-number"}'), finishChunk("tool_calls")),
|
sseEvents(
|
||||||
|
toolCallChunk("call_1", "generate_object", '{"value":"not-a-number"}'),
|
||||||
|
finishChunk("tool_calls"),
|
||||||
|
),
|
||||||
{ headers: { "content-type": "text/event-stream" } },
|
{ headers: { "content-type": "text/event-stream" } },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -181,9 +181,7 @@ describe("Cloudflare", () => {
|
|||||||
dynamicResponse((input) =>
|
dynamicResponse((input) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
||||||
expect(web.url).toBe(
|
expect(web.url).toBe("https://api.cloudflare.com/client/v4/accounts/test-account/ai/v1/chat/completions")
|
||||||
"https://api.cloudflare.com/client/v4/accounts/test-account/ai/v1/chat/completions",
|
|
||||||
)
|
|
||||||
expect(web.headers.get("authorization")).toBe("Bearer test-token")
|
expect(web.headers.get("authorization")).toBe("Bearer test-token")
|
||||||
expect(decodeJson(input.text)).toMatchObject({
|
expect(decodeJson(input.text)).toMatchObject({
|
||||||
model: "@cf/meta/llama-3.1-8b-instruct",
|
model: "@cf/meta/llama-3.1-8b-instruct",
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import {
|
import { ContentPart, LLMEvent, LLMRequest, ModelID, ModelLimits, ModelRef, ProviderID } from "../src/schema"
|
||||||
ContentPart,
|
|
||||||
LLMEvent,
|
|
||||||
LLMRequest,
|
|
||||||
ModelID,
|
|
||||||
ModelLimits,
|
|
||||||
ModelRef,
|
|
||||||
ProviderID,
|
|
||||||
} from "../src/schema"
|
|
||||||
|
|
||||||
const model = new ModelRef({
|
const model = new ModelRef({
|
||||||
id: ModelID.make("fake-model"),
|
id: ModelID.make("fake-model"),
|
||||||
|
|||||||
Reference in New Issue
Block a user