feat(ai): publish package

This commit is contained in:
Dax Raad
2026-07-15 15:17:33 -04:00
parent bb15b16499
commit 3fdd12fa81
23 changed files with 129 additions and 100 deletions
+1
View File
@@ -168,6 +168,7 @@
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
},
},
"packages/console/app": {
+8 -36
View File
@@ -4,47 +4,18 @@
"name": "@opencode-ai/ai",
"type": "module",
"license": "MIT",
"private": true,
"scripts": {
"setup:recording-env": "bun run script/setup-recording-env.ts",
"test": "bun test --timeout 30000 --only-failures",
"typecheck": "tsgo --noEmit"
"typecheck": "tsgo --noEmit",
"build": "tsc -p tsconfig.build.json"
},
"files": [
"dist"
],
"exports": {
".": "./src/index.ts",
"./route": "./src/route/index.ts",
"./provider": "./src/provider.ts",
"./providers": "./src/providers/index.ts",
"./provider-package": "./src/provider-package.ts",
"./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts",
"./providers/anthropic": "./src/providers/anthropic.ts",
"./providers/anthropic-compatible": "./src/providers/anthropic-compatible.ts",
"./providers/azure": "./src/providers/azure.ts",
"./providers/azure/responses": "./src/providers/azure/responses.ts",
"./providers/azure/chat": "./src/providers/azure/chat.ts",
"./providers/cloudflare": "./src/providers/cloudflare.ts",
"./providers/github-copilot": "./src/providers/github-copilot.ts",
"./providers/google": "./src/providers/google.ts",
"./providers/google-vertex": "./src/providers/google-vertex.ts",
"./providers/google-vertex/anthropic": "./src/providers/google-vertex/anthropic.ts",
"./providers/openai": "./src/providers/openai.ts",
"./providers/openai/responses": "./src/providers/openai/responses.ts",
"./providers/openai/chat": "./src/providers/openai/chat.ts",
"./providers/openai-compatible": "./src/providers/openai-compatible.ts",
"./providers/openai-compatible/responses": "./src/providers/openai-compatible-responses.ts",
"./providers/openai-compatible-profile": "./src/providers/openai-compatible-profile.ts",
"./providers/openrouter": "./src/providers/openrouter.ts",
"./providers/xai": "./src/providers/xai.ts",
"./protocols": "./src/protocols/index.ts",
"./protocols/anthropic-messages": "./src/protocols/anthropic-messages.ts",
"./protocols/bedrock-converse": "./src/protocols/bedrock-converse.ts",
"./protocols/gemini": "./src/protocols/gemini.ts",
"./protocols/google-vertex-anthropic": "./src/protocols/google-vertex-anthropic.ts",
"./protocols/google-vertex-gemini": "./src/protocols/google-vertex-gemini.ts",
"./protocols/openai-chat": "./src/protocols/openai-chat.ts",
"./protocols/openai-compatible-chat": "./src/protocols/openai-compatible-chat.ts",
"./protocols/openai-compatible-responses": "./src/protocols/openai-compatible-responses.ts",
"./protocols/openai-responses": "./src/protocols/openai-responses.ts"
"./*": "./src/*.ts"
},
"devDependencies": {
"@clack/prompts": "1.0.0-alpha.1",
@@ -52,7 +23,8 @@
"@opencode-ai/http-recorder": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:"
"@typescript/native-preview": "catalog:",
"typescript": "catalog:"
},
"dependencies": {
"@smithy/eventstream-codec": "4.2.14",
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
import { fileURLToPath } from "url"
const dir = fileURLToPath(new URL("..", import.meta.url))
process.chdir(dir)
async function published(name: string, version: string) {
return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0
}
await $`bun run build`
const originalText = await Bun.file("package.json").text()
const pkg = JSON.parse(originalText) as {
name: string
version: string
exports: Record<string, string>
}
if (await published(pkg.name, pkg.version)) {
console.log(`already published ${pkg.name}@${pkg.version}`)
} else {
for (const [key, value] of Object.entries(pkg.exports)) {
const file = value.replace("./src/", "./dist/").replace(".ts", "")
// @ts-ignore
pkg.exports[key] = {
import: file + ".js",
types: file + ".d.ts",
}
}
await Bun.write("package.json", JSON.stringify(pkg, null, 2))
try {
await $`bun pm pack`
await $`npm publish *.tgz --tag ${Script.channel} --access public`
} finally {
await Bun.write("package.json", originalText)
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ export type {
AnyTool,
ExecutableTool,
ExecutableTools,
Tool as ToolShape,
Definition as ToolShape,
ToolExecute,
ToolExecuteContext,
ToolModelOutputInput,
+1
View File
@@ -0,0 +1 @@
export * from "./protocols/index"
@@ -1,7 +1,7 @@
import { EventStreamCodec } from "@smithy/eventstream-codec"
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
import { Effect, Stream } from "effect"
import type { Framing } from "../route/framing"
import { Framing } from "../route/framing"
import { ProviderShared } from "./shared"
// Bedrock streams responses using the AWS event stream binary protocol — each
@@ -79,7 +79,7 @@ const consumeFrames = (route: string) => (state: FrameBufferState, chunk: Uint8A
* under its `:event-type` header so the chunk schema can match the JSON
* payload directly.
*/
export const framing = (route: string): Framing<object> => ({
export const framing = (route: string): Framing.Definition<object> => ({
id: "aws-event-stream",
frame: (bytes) => bytes.pipe(Stream.mapAccumEffect(() => initialFrameBuffer, consumeFrames(route))),
})
+1 -1
View File
@@ -160,7 +160,7 @@ export const OpenAIChatEvent = Schema.Struct({
export type OpenAIChatEvent = Schema.Schema.Type<typeof OpenAIChatEvent>
type OpenAIChatRequestMessage = LLMRequest["messages"][number]
interface ParserState {
export interface ParserState {
readonly tools: ToolStream.State<number>
readonly toolCallEvents: ReadonlyArray<LLMEvent>
readonly usage?: Usage
+1
View File
@@ -0,0 +1 @@
export * from "./providers/index"
@@ -6,7 +6,7 @@ const SCOPE = "https://www.googleapis.com/auth/cloud-platform"
export type OAuthOptions =
| { readonly accessToken?: string; readonly auth?: never }
| { readonly accessToken?: never; readonly auth?: Auth }
| { readonly accessToken?: never; readonly auth?: Auth.Definition }
export type ApiKeyOptions =
| (OAuthOptions & { readonly apiKey?: never })
@@ -0,0 +1 @@
export * from "../openai-compatible-responses"
+1
View File
@@ -0,0 +1 @@
export * from "./route/index"
+5 -2
View File
@@ -4,7 +4,7 @@ import { Auth } from "./auth"
export type ApiKeyMode = "optional" | "required"
export type AuthOverride = {
readonly auth: Auth
readonly auth: Auth.Definition
readonly apiKey?: never
}
@@ -44,7 +44,10 @@ export type AtLeastOne<T> = {
* override, otherwise resolve `apiKey` (option > config var) and apply it as
* a bearer token.
*/
export const bearer = (options: ProviderAuthOption<"optional">, envVar: string | ReadonlyArray<string>): Auth => {
export const bearer = (
options: ProviderAuthOption<"optional">,
envVar: string | ReadonlyArray<string>,
): Auth.Definition => {
if ("auth" in options && options.auth) return options.auth
return (Array.isArray(envVar) ? envVar : [envVar])
.reduce(
+15 -15
View File
@@ -25,19 +25,19 @@ export interface AuthInput {
export interface Credential {
readonly load: Effect.Effect<Redacted.Redacted, CredentialError>
readonly orElse: (that: Credential) => Credential
readonly bearer: () => Auth
readonly header: (name: string) => Auth
readonly bearer: () => Definition
readonly header: (name: string) => Definition
readonly pipe: <A>(f: (self: Credential) => A) => A
}
export interface Auth {
export interface Definition {
readonly apply: (input: AuthInput) => Effect.Effect<Headers.Headers, AuthError>
readonly andThen: (that: Auth) => Auth
readonly orElse: (that: Auth) => Auth
readonly pipe: <A>(f: (self: Auth) => A) => A
readonly andThen: (that: Definition) => Definition
readonly orElse: (that: Definition) => Definition
readonly pipe: <A>(f: (self: Definition) => A) => A
}
export const isAuth = (input: unknown): input is Auth =>
export const isAuth = (input: unknown): input is Definition =>
typeof input === "object" && input !== null && "apply" in input && typeof input.apply === "function"
const credential = (load: Effect.Effect<Redacted.Redacted, CredentialError>): Credential => {
@@ -51,8 +51,8 @@ const credential = (load: Effect.Effect<Redacted.Redacted, CredentialError>): Cr
return self
}
const auth = (apply: Auth["apply"]): Auth => {
const self: Auth = {
const auth = (apply: Definition["apply"]): Definition => {
const self: Definition = {
apply,
andThen: (that) =>
auth((input) => apply(input).pipe(Effect.flatMap((headers) => that.apply({ ...input, headers })))),
@@ -109,15 +109,15 @@ const credentialInput = (source: Secret | Credential) =>
? credentialFromSecret(source, "value")
: source
export function bearer(source: Secret | Credential): Auth
export function bearer(source: Secret | Credential): Definition
export function bearer(source: Secret | Credential) {
return credentialInput(source).bearer()
}
export const apiKey = bearer
export function header(name: string): (source: Secret | Credential) => Auth
export function header(name: string, source: Secret | Credential): Auth
export function header(name: string): (source: Secret | Credential) => Definition
export function header(name: string, source: Secret | Credential): Definition
export function header(name: string, source?: Secret | Credential) {
if (source === undefined) {
return (next: Secret | Credential) => credentialInput(next).header(name)
@@ -125,8 +125,8 @@ export function header(name: string, source?: Secret | Credential) {
return credentialInput(source).header(name)
}
export function bearerHeader(name: string): (source: Secret | Credential) => Auth
export function bearerHeader(name: string, source: Secret | Credential): Auth
export function bearerHeader(name: string): (source: Secret | Credential) => Definition
export function bearerHeader(name: string, source: Secret | Credential): Definition
export function bearerHeader(name: string, source?: Secret | Credential) {
const render = (input: Secret | Credential) =>
fromCredential(credentialInput(input), (secret) => ({ [name]: `Bearer ${secret}` }))
@@ -149,7 +149,7 @@ const toLLMError = (error: AuthError): LLMError => {
}
export const toEffect =
(input: Auth) =>
(input: Definition) =>
(authInput: AuthInput): Effect.Effect<Headers.Headers, LLMError> =>
input.apply(authInput).pipe(Effect.mapError(toLLMError))
+11 -11
View File
@@ -1,9 +1,9 @@
import { Cause, Context, Effect, Layer, Schema, Stream } from "effect"
import * as Option from "effect/Option"
import { Auth, type Auth as AuthDef } from "./auth"
import { Auth } from "./auth"
import { Endpoint, type EndpointPatch } from "./endpoint"
import { RequestExecutor } from "./executor"
import type { Framing } from "./framing"
import { Framing } from "./framing"
import { HttpTransport } from "./transport"
import type { Transport, TransportRuntime } from "./transport"
import { WebSocketExecutor } from "./transport"
@@ -40,8 +40,8 @@ export interface Route<Body, Prepared = unknown> {
/** ProviderMetadata namespace emitted and consumed by this route. */
readonly providerMetadataKey?: string
readonly protocol: ProtocolID
readonly endpoint: Endpoint<Body>
readonly auth: AuthDef
readonly endpoint: Endpoint.Definition<Body>
readonly auth: Auth.Definition
readonly transport: Transport<Body, Prepared, unknown>
readonly defaults: RouteDefaults
readonly body: RouteBody<Body>
@@ -86,7 +86,7 @@ export interface RouteDefaultsInput {
export interface RoutePatch<Body, Prepared> extends RouteDefaultsInput {
readonly id?: string
readonly provider?: string | ProviderID
readonly auth?: AuthDef
readonly auth?: Auth.Definition
readonly transport?: Transport<Body, Prepared, unknown>
readonly endpoint?: EndpointPatch<Body>
}
@@ -122,7 +122,7 @@ const mergeRouteDefaults = (base: RouteDefaults | undefined, patch: RouteDefault
}
}
const endpointBaseURL = <Body>(endpoint: Endpoint<Body>) =>
const endpointBaseURL = <Body>(endpoint: Endpoint.Definition<Body>) =>
typeof endpoint.baseURL === "string" ? endpoint.baseURL : undefined
const mergeHeaders = (...items: ReadonlyArray<Record<string, string> | undefined>) => {
@@ -192,11 +192,11 @@ export interface MakeInput<Body, Frame, Event, State> {
/** Semantic API contract — owns body construction, body schema, and parsing. */
readonly protocol: Protocol<Body, Frame, Event, State>
/** Where the request is sent. */
readonly endpoint: Endpoint<Body>
readonly endpoint: Endpoint.Definition<Body>
/** Per-request transport auth. Provider facades override this via `route.with(...)`. */
readonly auth?: AuthDef
readonly auth?: Auth.Definition
/** Stream framing — bytes -> frames before `protocol.stream.event` decoding. */
readonly framing: Framing<Frame>
readonly framing: Framing.Definition<Frame>
/** Static / per-request headers added before `auth` runs. */
readonly headers?: (input: { readonly request: LLMRequest }) => Record<string, string>
/** Route/request defaults used when compiling requests for this route. */
@@ -213,9 +213,9 @@ export interface MakeTransportInput<Body, Prepared, Frame, Event, State> {
/** Semantic API contract — owns body construction, body schema, and parsing. */
readonly protocol: Protocol<Body, Frame, Event, State>
/** Where the request is sent. */
readonly endpoint: Endpoint<Body>
readonly endpoint: Endpoint.Definition<Body>
/** Per-request transport auth. Provider facades override this via `route.with(...)`. */
readonly auth?: AuthDef
readonly auth?: Auth.Definition
/** Static / per-request headers added before `auth` runs. */
readonly headers?: (input: { readonly request: LLMRequest }) => Record<string, string>
/** Runnable transport route. */
+8 -5
View File
@@ -19,21 +19,24 @@ export type EndpointPart<Body> = string | ((input: EndpointInput<Body>) => strin
* URL embeds the model id, region, or another body field (e.g. Bedrock,
* Gemini).
*/
export interface Endpoint<Body> {
export interface Definition<Body> {
readonly baseURL?: string
readonly path: EndpointPart<Body>
readonly query?: Record<string, string>
}
export type EndpointPatch<Body> = Partial<Endpoint<Body>>
export type EndpointPatch<Body> = Partial<Definition<Body>>
/** Construct an `Endpoint` from a path string or path function. */
export const path = <Body>(value: EndpointPart<Body>, options: Omit<Endpoint<Body>, "path"> = {}): Endpoint<Body> => ({
export const path = <Body>(
value: EndpointPart<Body>,
options: Omit<Definition<Body>, "path"> = {},
): Definition<Body> => ({
...options,
path: value,
})
export const merge = <Body>(base: Endpoint<Body>, patch: EndpointPatch<Body>): Endpoint<Body> => ({
export const merge = <Body>(base: Definition<Body>, patch: EndpointPatch<Body>): Definition<Body> => ({
...base,
...patch,
baseURL: patch.baseURL ?? base.baseURL,
@@ -44,7 +47,7 @@ export const merge = <Body>(base: Endpoint<Body>, patch: EndpointPatch<Body>): E
const renderPart = <Body>(part: EndpointPart<Body>, input: EndpointInput<Body>) =>
typeof part === "function" ? part(input) : part
export const render = <Body>(endpoint: Endpoint<Body>, input: EndpointInput<Body>) => {
export const render = <Body>(endpoint: Definition<Body>, input: EndpointInput<Body>) => {
const url = new URL(`${ProviderShared.trimBaseUrl(endpoint.baseURL ?? "")}${renderPart(endpoint.path, input)}`)
for (const [key, value] of Object.entries(endpoint.query ?? {})) url.searchParams.set(key, value)
return url
+2 -2
View File
@@ -16,12 +16,12 @@ import type { LLMError } from "../schema"
* The frame type is opaque to this layer; the protocol's `decode` step turns
* a frame into a typed chunk.
*/
export interface Framing<Frame> {
export interface Definition<Frame> {
readonly id: string
readonly frame: (bytes: Stream.Stream<Uint8Array, LLMError>) => Stream.Stream<Frame, LLMError>
}
/** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
export const sse: Framing<string> = { id: "sse", frame: ProviderShared.sseFraming }
export const sse: Definition<string> = { id: "sse", frame: ProviderShared.sseFraming }
export * as Framing from "./framing"
+3 -3
View File
@@ -17,9 +17,9 @@ export { Framing } from "./framing"
export { Protocol } from "./protocol"
export { HttpTransport, WebSocketExecutor, WebSocketTransport } from "./transport"
export * as Transport from "./transport"
export type { Auth as AuthShape, AuthInput, Credential, CredentialError } from "./auth"
export type { Definition as AuthShape, AuthInput, Credential, CredentialError } from "./auth"
export type { ApiKeyMode, AuthOverride, ProviderAuthOption } from "./auth-options"
export type { Endpoint as EndpointFn, EndpointInput } from "./endpoint"
export type { Framing as FramingDef } from "./framing"
export type { Definition as EndpointFn, EndpointInput } from "./endpoint"
export type { Definition as FramingDef } from "./framing"
export type { Protocol as ProtocolDef } from "./protocol"
export type { Transport as TransportDef, TransportRuntime } from "./transport"
+3 -3
View File
@@ -2,7 +2,7 @@ import { Effect, Stream } from "effect"
import { Headers, HttpClientRequest } from "effect/unstable/http"
import { Auth } from "../auth"
import { render as renderEndpoint } from "../endpoint"
import { Framing, type Framing as FramingDef } from "../framing"
import { Framing } from "../framing"
import type { Transport, TransportPrepareInput } from "./index"
import * as ProviderShared from "../../protocols/shared"
import { mergeJsonRecords, type LLMRequest } from "../../schema"
@@ -18,7 +18,7 @@ export interface JsonRequestParts<Body = unknown> {
export interface HttpPrepared<Frame> {
readonly request: HttpClientRequest.HttpClientRequest
readonly framing: FramingDef<Frame>
readonly framing: Framing.Definition<Frame>
}
const applyQuery = (url: string, query: Record<string, string> | undefined) => {
@@ -107,7 +107,7 @@ export const jsonRequestParts = <Body>(input: JsonRequestInput<Body>) =>
})
export interface HttpJsonInput<_Body, Frame> {
readonly framing: FramingDef<Frame>
readonly framing: Framing.Definition<Frame>
}
export type HttpJsonPatch<Body, Frame> = Partial<HttpJsonInput<Body, Frame>>
+4 -4
View File
@@ -1,6 +1,6 @@
import type { Effect, Stream } from "effect"
import type { Endpoint } from "../endpoint"
import type { Auth } from "../auth"
import { Endpoint } from "../endpoint"
import { Auth } from "../auth"
import type { Interface as RequestExecutorInterface } from "../executor"
import type { Interface as WebSocketExecutorInterface } from "./websocket"
import type { LLMError, LLMRequest } from "../../schema"
@@ -23,8 +23,8 @@ export interface Transport<Body, Prepared, Frame> {
export interface TransportPrepareInput<Body> {
readonly body: Body
readonly request: LLMRequest
readonly endpoint: Endpoint<Body>
readonly auth: Auth
readonly endpoint: Endpoint.Definition<Body>
readonly auth: Auth.Definition
readonly encodeBody: (body: Body) => string
readonly headers?: (input: { readonly request: LLMRequest }) => Record<string, string>
}
+4 -4
View File
@@ -45,7 +45,7 @@ export type ToolToModelOutput<Parameters extends ToolSchema<any>, Success extend
* Internally each tool also carries memoized codecs and a precomputed
* `ToolDefinition` so callers do not rebuild them per invocation.
*/
export interface Tool<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> {
export interface Definition<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> {
readonly description: string
readonly parameters: Parameters
readonly success: Success
@@ -68,9 +68,9 @@ export interface Tool<Parameters extends ToolSchema<any>, Success extends ToolSc
readonly _definition: ToolDefinitionClass
}
export type AnyTool = Tool<any, any>
export type AnyTool = Definition<any, any>
export type ExecutableTool<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = Tool<
export type ExecutableTool<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = Definition<
Parameters,
Success
> & {
@@ -145,7 +145,7 @@ export function make<Parameters extends ToolSchema<any>, Success extends ToolSch
readonly execute?: undefined
readonly toModelOutput?: ToolToModelOutput<Parameters, Success>
readonly toStructuredOutput?: (output: Success["Encoded"]) => unknown
}): Tool<Parameters, Success>
}): Definition<Parameters, Success>
export function make(config: {
readonly description: string
readonly jsonSchema: JsonSchema.JsonSchema
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": false,
"noEmit": false
}
}
+7 -10
View File
@@ -1,15 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"noUncheckedIndexedAccess": false,
"plugins": [
{
"name": "@effect/language-service",
"transform": "@effect/language-service/transform",
"namespaceImportPackages": ["effect", "@effect/*"]
}
]
}
"noUncheckedIndexedAccess": false
},
"include": ["src"]
}
+3
View File
@@ -38,6 +38,9 @@ await prepareReleaseFiles()
console.log("\n=== schema ===\n")
await $`bun ./packages/schema/script/publish.ts`
console.log("\n=== ai ===\n")
await $`bun ./packages/ai/script/publish.ts`
console.log("\n=== protocol ===\n")
await $`bun ./packages/protocol/script/publish.ts`