diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..b5093d01 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,12 @@ +FROM oven/bun:debian + +# Config Bun +ENV PATH="~/.bun/bin:${PATH}" +RUN ln -s /usr/local/bin/bun /usr/local/bin/node + +# Update packages +RUN if [ "debian" == "alpine" ] ; then apk update ; else apt-get update ; fi + +# Install Git +RUN if [ "debian" == "alpine" ] ; then apk add git ; else apt-get install -y git ; fi + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..f80bcfa4 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,16 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/marcosgomesneto/bun-devcontainers/tree/main/src/basic-bun +{ + "name": "Bun", + "dockerFile": "Dockerfile", + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "oven.bun-vscode" + ] + } + } +} \ No newline at end of file diff --git a/packages/core/effect-drizzle-sqlite/src/effect-sqlite/driver.ts.gcov.html b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/driver.ts.gcov.html new file mode 100644 index 00000000..b3aa72ec --- /dev/null +++ b/packages/core/effect-drizzle-sqlite/src/effect-sqlite/driver.ts.gcov.html @@ -0,0 +1,153 @@ + + + + +
+ +| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 42 : import * as Effect from "effect/Effect" + 3 76 : import * as Layer from "effect/Layer" + 4 116 : import { SqlClient } from "effect/unstable/sql/SqlClient" + 5 132 : import { EffectCache } from "drizzle-orm/cache/core/cache-effect" + 6 110 : import { EffectLogger } from "drizzle-orm/effect-core" + 7 96 : import { entityKind } from "drizzle-orm/entity" + 8 : import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations" + 9 138 : import { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect" + 10 128 : import { SQLiteEffectDatabase } from "../sqlite-core/effect/db" + 11 : import type { DrizzleConfig } from "drizzle-orm/utils" + 12 118 : import { jitCompatCheck } from "../internal/drizzle-utils" + 13 96 : import { type EffectSQLiteQueryEffectHKT, type EffectSQLiteRunResult, EffectSQLiteSession } from "./session" + 14 : + 15 124 : export class EffectSQLiteDatabase<TRelations extends AnyRelations = EmptyRelations> extends SQLiteEffectDatabase< + 16 : EffectSQLiteQueryEffectHKT, + 17 : EffectSQLiteRunResult, + 18 : TRelations + 19 22 : > { + 20 76 : static override readonly [entityKind]: string = "EffectSQLiteDatabase" + 21 2 : } + 22 : + 23 : export type EffectDrizzleSQLiteConfig<TRelations extends AnyRelations = EmptyRelations> = Omit< + 24 : DrizzleConfig<Record<string, never>, TRelations>, + 25 : "cache" | "logger" | "schema" + 26 : > + 27 : + 28 172 : export const DefaultServices = Layer.merge(EffectCache.Default, EffectLogger.Default) + 29 : + 30 : /** + 31 : * Creates an EffectSQLiteDatabase instance. + 32 : * + 33 : * Requires a generic Effect `SqlClient`, `EffectLogger`, and `EffectCache` services to be provided. + 34 : * Drizzle only depends on the generic `SqlClient`; install and provide a compatible SQLite provider such as + 35 : * `@effect/sql-sqlite-node`, `@effect/sql-sqlite-bun`, or another package that exposes `SqlClient`. + 36 : * + 37 : * @example + 38 : * ```ts + 39 : * import { SqliteClient } from '@effect/sql-sqlite-node'; + 40 : * import * as SQLiteDrizzle from 'drizzle-orm/effect-sqlite'; + 41 : * import * as Effect from 'effect/Effect'; + 42 : * + 43 : * const db = yield* SQLiteDrizzle.make({ relations }).pipe( + 44 : * Effect.provide(SQLiteDrizzle.DefaultServices), + 45 : * Effect.provide(SqliteClient.layer({ filename: 'sqlite.db' })), + 46 : * ); + 47 : * ``` + 48 : */ + 49 113 : export const make = Effect.fn("SQLiteDrizzle.make")(function* <TRelations extends AnyRelations = EmptyRelations>( + 50 25 : config: EffectDrizzleSQLiteConfig<TRelations> = {}, + 51 6 : ) { + 52 68 : const client = yield* SqlClient + 53 70 : const cache = yield* EffectCache + 54 74 : const logger = yield* EffectLogger + 55 : + 56 82 : const dialect = new SQLiteAsyncDialect() + 57 86 : const relations = config.relations ?? ({} as TRelations) + 58 146 : const session = new EffectSQLiteSession(client, dialect, relations, { + 59 22 : logger, + 60 20 : cache, + 61 86 : useJitMappers: jitCompatCheck(config.jit), + 62 10 : }) + 63 134 : const db = new EffectSQLiteDatabase(dialect, session, relations) as EffectSQLiteDatabase<TRelations> & { + 64 : $client: SqlClient + 65 : } + 66 44 : db.$client = client + 67 80 : db.$cache.invalidate = cache.onMutate + 68 : + 69 20 : return db + 70 6 : }) + 71 : + 72 : /** + 73 : * Convenience function that creates an EffectSQLiteDatabase with `DefaultServices` already provided. + 74 : */ + 75 63 : export const makeWithDefaults = <TRelations extends AnyRelations = EmptyRelations>( + 76 32 : config: EffectDrizzleSQLiteConfig<TRelations> = {}, + 77 102 : ) => make(config).pipe(Effect.provide(DefaultServices)) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| driver.ts | +
+ |
+ 100.0 % | +36 | +36 | +|
| migrator.ts | +
+ |
+ 28.6 % | +7 | +2 | +|
| session.ts | +
+ |
+ 83.4 % | +151 | +126 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| migrator.ts | +
+ |
+ 28.6 % | +7 | +2 | +|
| session.ts | +
+ |
+ 83.4 % | +151 | +126 | +|
| driver.ts | +
+ |
+ 100.0 % | +36 | +36 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| driver.ts | +
+ |
+ 100.0 % | +36 | +36 | +|
| migrator.ts | +
+ |
+ 28.6 % | +7 | +2 | +|
| session.ts | +
+ |
+ 83.4 % | +151 | +126 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 : import type { MigrationConfig } from "drizzle-orm/migrator" + 3 58 : import { readMigrationFiles } from "drizzle-orm/migrator" + 4 : import type { AnyRelations } from "drizzle-orm/relations" + 5 71 : import { migrate as coreMigrate } from "../sqlite-core/effect/session" + 6 : import type { EffectSQLiteDatabase } from "./driver" + 7 : + 8 0 : export function migrate<TRelations extends AnyRelations>( + 9 0 : db: EffectSQLiteDatabase<TRelations>, + 10 0 : config: MigrationConfig, + 11 0 : ) { + 12 0 : const migrations = readMigrationFiles(config) + 13 : return coreMigrate(migrations, db.session, config) + 14 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 46 : import * as Context from "effect/Context" + 3 80 : import * as Effect from "effect/Effect" + 4 72 : import * as Exit from "effect/Exit" + 5 76 : import * as Scope from "effect/Scope" + 6 : import type { SqlClient } from "effect/unstable/sql/SqlClient" + 7 : import type { SqlError } from "effect/unstable/sql/SqlError" + 8 : import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect" + 9 : import type { WithCacheConfig } from "drizzle-orm/cache/core/types" + 10 : import type { EffectDrizzleQueryError } from "drizzle-orm/effect-core/errors" + 11 : import type { EffectLoggerShape } from "drizzle-orm/effect-core/logger" + 12 : import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" + 13 96 : import { entityKind } from "drizzle-orm/entity" + 14 : import type { AnyRelations } from "drizzle-orm/relations" + 15 : import type { RelationalQueryMapperConfig } from "drizzle-orm/relations" + 16 : import type { Query } from "drizzle-orm/sql/sql" + 17 : import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect" + 18 240 : import { SQLiteEffectPreparedQuery, SQLiteEffectSession, SQLiteEffectTransaction } from "../sqlite-core/effect/session" + 19 : import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types" + 20 : import type { PreparedQueryConfig, SQLiteExecuteMethod, SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session" + 21 : + 22 : export interface EffectSQLiteQueryEffectHKT extends QueryEffectHKTBase { + 23 : readonly error: EffectDrizzleQueryError + 24 : readonly context: never + 25 : } + 26 : + 27 : export type EffectSQLiteRunResult = readonly never[] + 28 : + 29 : export interface EffectSQLiteSessionOptions { + 30 : logger: EffectLoggerShape + 31 : cache: EffectCacheShape + 32 : useJitMappers?: boolean + 33 : } + 34 : + 35 120 : export class EffectSQLiteSession<TRelations extends AnyRelations> extends SQLiteEffectSession< + 36 : EffectSQLiteQueryEffectHKT, + 37 : EffectSQLiteRunResult, + 38 : TRelations + 39 6 : > { + 40 75 : static override readonly [entityKind]: string = "EffectSQLiteSession" + 41 : + 42 13 : constructor( + 43 84 : private client: SqlClient, + 44 18 : dialect: SQLiteAsyncDialect, + 45 108 : protected relations: TRelations, + 46 108 : private options: EffectSQLiteSessionOptions, + 47 10 : ) { + 48 38 : super(dialect) + 49 : } + 50 : + 51 14 : override prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>( + 52 14 : query: Query, + 53 16 : fields: SelectedFieldsOrdered | undefined, + 54 30 : executeMethod: SQLiteExecuteMethod, + 55 40 : customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown, + 56 30 : queryMetadata?: { + 57 : type: "select" | "update" | "delete" | "insert" + 58 : tables: string[] + 59 : }, + 60 26 : cacheConfig?: WithCacheConfig, + 61 10 : ): SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT> { + 62 73 : return new SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT>( + 63 113 : (params, method) => this.execute(query, params, method), + 64 14 : query, + 65 42 : this.options.logger, + 66 40 : this.options.cache, + 67 30 : queryMetadata, + 68 26 : cacheConfig, + 69 16 : fields, + 70 30 : executeMethod, + 71 56 : this.options.useJitMappers, + 72 40 : customResultMapper, + 73 22 : undefined, + 74 22 : undefined, + 75 44 : this.isInTransaction(), + 76 9 : ) + 77 : } + 78 : + 79 0 : override prepareRelationalQuery<T extends PreparedQueryConfig = PreparedQueryConfig>( + 80 0 : query: Query, + 81 0 : fields: SelectedFieldsOrdered | undefined, + 82 0 : executeMethod: SQLiteExecuteMethod, + 83 0 : customResultMapper: (rows: Record<string, unknown>[], mapColumnValue?: (value: unknown) => unknown) => unknown, + 84 0 : config: RelationalQueryMapperConfig, + 85 0 : ): SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT, true> { + 86 0 : return new SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT, true>( + 87 0 : (params, method) => this.execute(query, params, method), + 88 0 : query, + 89 0 : this.options.logger, + 90 0 : this.options.cache, + 91 0 : undefined, + 92 0 : undefined, + 93 0 : fields, + 94 0 : executeMethod, + 95 0 : this.options.useJitMappers, + 96 0 : customResultMapper, + 97 0 : true, + 98 0 : config, + 99 0 : this.isInTransaction(), + 100 8 : ) + 101 : } + 102 : + 103 65 : private execute(query: Query, params: unknown[], method: SQLiteExecuteMethod | "values") { + 104 120 : const statement = this.client.unsafe(query.sql, params) + 105 114 : if (method === "values") return statement.values + 106 198 : if (method === "get") return statement.withoutTransform.pipe(Effect.map((rows) => rows[0])) + 107 73 : return statement.withoutTransform + 108 : } + 109 : + 110 31 : private isInTransaction() { + 111 229 : return Effect.serviceOption(this.client.transactionService).pipe(Effect.map((option) => option._tag === "Some")) + 112 : } + 113 : + 114 77 : private executeTransactionStatement(connection: Effect.Success<SqlClient["reserve"]>, query: string) { + 115 161 : return connection.executeUnprepared(query, [], undefined).pipe(Effect.asVoid) + 116 : } + 117 : + 118 59 : private withTransaction<A, E, R>(effect: Effect.Effect<A, E, R>, config: SQLiteTransactionConfig | undefined) { + 119 93 : return Effect.uninterruptibleMask((restore) => + 120 69 : Effect.withFiber<A, E | SqlError, R>((fiber) => { + 121 74 : const services = fiber.context + 122 182 : const connectionOption = Context.getOption(services, this.client.transactionService) + 123 38 : const connection: Effect.Effect< + 124 : readonly [Scope.Closeable | undefined, Effect.Success<SqlClient["reserve"]>], + 125 : SqlError + 126 : > = + 127 69 : connectionOption._tag === "Some" + 128 59 : ? Effect.succeed([undefined, connectionOption.value[0]] as const) + 129 36 : : Scope.make().pipe( + 130 51 : Effect.flatMap((scope) => + 131 94 : Scope.provide(this.client.reserve, scope).pipe( + 132 96 : Effect.map((connection) => [scope, connection] as const), + 133 0 : Effect.catch((error) => + 134 77 : Scope.close(scope, Exit.fail(error)).pipe(Effect.andThen(Effect.fail(error))), + 135 2 : ), + 136 1 : ), + 137 2 : ), + 138 15 : ) + 139 140 : const id = connectionOption._tag === "Some" ? connectionOption.value[1] + 1 : 0 + 140 : + 141 46 : return connection.pipe( + 142 97 : Effect.flatMap(([scope, connection]) => { + 143 106 : const transaction = this.executeTransactionStatement( + 144 24 : connection, + 145 134 : id === 0 ? `begin ${config?.behavior ?? "deferred"}` : `savepoint effect_sql_${id}`, + 146 13 : ).pipe( + 147 41 : Effect.flatMap(() => + 148 44 : Effect.provideContext( + 149 34 : restore(effect), + 150 142 : Context.add(services, this.client.transactionService, [connection, id]), + 151 14 : ).pipe( + 152 26 : Effect.exit, + 153 71 : Effect.flatMap((exit) => { + 154 79 : const finalize = Exit.isSuccess(exit) + 155 21 : ? id === 0 + 156 120 : ? this.executeTransactionStatement(connection, "commit").pipe( + 157 : // SQLite keeps the transaction open after deferred constraint commit failures. + 158 0 : Effect.catch((error) => + 159 0 : this.executeTransactionStatement(connection, "rollback").pipe( + 160 0 : Effect.catch(() => Effect.void), + 161 34 : Effect.andThen(Effect.fail(error)), + 162 1 : ), + 163 2 : ), + 164 5 : ) + 165 87 : : this.executeTransactionStatement(connection, `release savepoint effect_sql_${id}`) + 166 21 : : id === 0 + 167 115 : ? this.executeTransactionStatement(connection, "rollback") + 168 92 : : this.executeTransactionStatement(connection, `rollback to savepoint effect_sql_${id}`).pipe( + 169 15 : Effect.andThen( + 170 82 : this.executeTransactionStatement(connection, `release savepoint effect_sql_${id}`), + 171 1 : ), + 172 22 : ) + 173 : + 174 104 : return finalize.pipe(Effect.flatMap(() => exit)) + 175 2 : }), + 176 1 : ), + 177 2 : ), + 178 20 : ) + 179 : + 180 57 : return scope === undefined + 181 16 : ? transaction + 182 140 : : transaction.pipe(Effect.onExit((exit) => Scope.close(scope, exit))) + 183 2 : }), + 184 8 : ) + 185 1 : }), + 186 9 : ) + 187 : } + 188 : + 189 13 : override transaction<A, E, R>( + 190 26 : transaction: (tx: EffectSQLiteTransaction<TRelations>) => Effect.Effect<A, E, R>, + 191 16 : config?: SQLiteTransactionConfig, + 192 10 : ): Effect.Effect<A, E | SqlError, R> { + 193 80 : const { dialect, relations } = this + 194 : + 195 56 : return this.withTransaction( + 196 82 : Effect.gen({ self: this }, function* () { + 197 142 : const tx = new EffectSQLiteTransaction<TRelations>(dialect, this, relations) + 198 : + 199 65 : return yield* transaction(tx) + 200 6 : }), + 201 12 : config, + 202 8 : ) + 203 : } + 204 2 : } + 205 : + 206 136 : export class EffectSQLiteTransaction<TRelations extends AnyRelations> extends SQLiteEffectTransaction< + 207 : EffectSQLiteQueryEffectHKT, + 208 : EffectSQLiteRunResult, + 209 : TRelations + 210 22 : > { + 211 84 : static override readonly [entityKind]: string = "EffectSQLiteTransaction" + 212 : + 213 27 : override transaction: <A, E, R>( + 214 : transaction: ( + 215 : tx: SQLiteEffectTransaction<EffectSQLiteQueryEffectHKT, EffectSQLiteRunResult, TRelations>, + 216 : ) => Effect.Effect<A, E, R>, + 217 40 : ) => Effect.Effect<A, SqlError | E, R> = (tx) => this.session.transaction(tx) + 218 1 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| index.ts | +
+ |
+ 100.0 % | +5 | +5 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 55 : export { EffectLogger } from "drizzle-orm/effect-core" + 2 39 : export * from "./effect-sqlite/driver" + 3 40 : export * from "./effect-sqlite/session" + 4 51 : export { migrate } from "./effect-sqlite/migrator" + 5 : + 6 40 : export * as EffectDrizzleSqlite from "." ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 60 : import { Column, getColumnTable } from "drizzle-orm/column" + 3 40 : import { is } from "drizzle-orm/entity" + 4 : import type { JoinNullability } from "drizzle-orm/query-builders/select.types" + 5 49 : import { Param, SQL } from "drizzle-orm/sql/sql" + 6 : import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types" + 7 : import type { SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update" + 8 : import type { SQLiteTable } from "drizzle-orm/sqlite-core/table" + 9 67 : import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base" + 10 48 : import { Subquery } from "drizzle-orm/subquery" + 11 56 : import { Table, getTableName } from "drizzle-orm/table" + 12 : import type { UpdateSet } from "drizzle-orm/utils" + 13 57 : import { ViewBaseConfig } from "drizzle-orm/view-common" + 14 : + 15 19 : const TableSymbol = ( + 16 6 : Table as unknown as { + 17 : Symbol: { Columns: symbol; IsAlias: symbol; Name: symbol; BaseName: symbol } + 18 : } + 19 8 : ).Symbol + 20 : + 21 16 : export function getTableColumnsRuntime(table: SQLiteTable) { + 22 34 : return (table as unknown as Record<symbol, Record<string, Column>>)[TableSymbol.Columns] + 23 : } + 24 : + 25 0 : export function getViewSelectedFieldsRuntime(view: SQLiteViewBase) { + 26 1 : return (view as unknown as Record<symbol, { selectedFields: Record<string, unknown>; name: string }>)[ViewBaseConfig] + 27 : } + 28 : + 29 20 : export function jitCompatCheck(isEnabled: boolean | undefined) { + 30 30 : if (!isEnabled) return false + 31 0 : try { + 32 0 : return new Function("input", '"use strict"; return input;')(true) === true + 33 0 : } catch { + 34 1 : return false + 35 : } + 36 : } + 37 : + 38 6 : export function orderSelectedFields<TColumn extends Column>( + 39 8 : fields: Record<string, unknown>, + 40 12 : pathPrefix?: string[], + 41 3 : ): SelectedFieldsOrdered { + 42 61 : return Object.entries(fields).flatMap(([name, field]) => { + 43 37 : const path = pathPrefix ? [...pathPrefix, name] : [name] + 44 96 : if (is(field, Column) || is(field, SQL) || is(field, SQL.Aliased) || is(field, Subquery)) { + 45 24 : return [{ path, field }] as SelectedFieldsOrdered + 46 0 : } + 47 0 : if (is(field, Table)) return orderSelectedFields(getTableColumnsRuntime(field as SQLiteTable), path) + 48 0 : return orderSelectedFields(field as Record<string, unknown>, path) + 49 2 : }) as SelectedFieldsOrdered + 50 : } + 51 : + 52 24 : export function mapUpdateSet<TTable extends SQLiteTable>(table: TTable, values: SQLiteUpdateSetSource<TTable>) { + 53 82 : const entries = Object.entries(values).filter(([, value]) => value !== undefined) + 54 30 : if (entries.length === 0) throw new Error("No values to set") + 55 : + 56 26 : return Object.fromEntries( + 57 34 : entries.map(([key, value]) => [ + 58 8 : key, + 59 92 : is(value, SQL) || is(value, Column) ? value : new Param(value, getTableColumnsRuntime(table)[key]), + 60 1 : ]), + 61 2 : ) as UpdateSet + 62 : } + 63 : + 64 6 : export function mapResultRow( + 65 9 : columns: SelectedFieldsOrdered, + 66 5 : row: unknown[], + 67 21 : joinsNotNullableMap: Record<string, boolean> | undefined, + 68 3 : ) { + 69 24 : const nullifyMap: Record<string, string | false> = {} + 70 20 : const result: Record<string, unknown> = {} + 71 : + 72 45 : columns.forEach((column, columnIndex) => { + 73 16 : const decoder = ( + 74 26 : is(column.field, Column) + 75 12 : ? column.field + 76 0 : : is(column.field, SQL) + 77 0 : ? (column.field as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder + 78 0 : : is(column.field, Subquery) + 79 0 : ? (column.field._.sql as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder + 80 4 : : (column.field.sql as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder + 81 : ) as { + 82 : mapFromDriverValue(value: unknown): unknown + 83 : } + 84 38 : const rawValue = row[columnIndex] + 85 79 : const value = rawValue === null ? null : decoder.mapFromDriverValue(rawValue) + 86 38 : const objectName = column.path[0] + 87 22 : let node = result + 88 : + 89 57 : column.path.forEach((pathChunk, pathChunkIndex) => { + 90 55 : if (pathChunkIndex === column.path.length - 1) { + 91 32 : node[pathChunk] = value + 92 6 : return + 93 0 : } + 94 0 : node[pathChunk] = (node[pathChunk] ?? {}) as Record<string, unknown> + 95 0 : node = node[pathChunk] as Record<string, unknown> + 96 6 : }) + 97 : + 98 94 : if (joinsNotNullableMap && is(column.field, Column) && column.path.length === 2 && objectName) { + 99 0 : const tableName = getTableName(getColumnTable(column.field)) + 100 0 : nullifyMap[objectName] = + 101 0 : !(objectName in nullifyMap) && value === null + 102 0 : ? tableName + 103 0 : : typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== tableName + 104 0 : ? false + 105 0 : : nullifyMap[objectName] + 106 2 : } + 107 4 : }) + 108 : + 109 0 : Object.entries(nullifyMap).forEach(([objectName, tableName]) => { + 110 : if (typeof tableName === "string" && !joinsNotNullableMap?.[tableName]) result[objectName] = null + 111 4 : }) + 112 : + 113 14 : return result + 114 : } + 115 : + 116 0 : export function getTableLikeName(table: SQLiteTable | Subquery | SQLiteViewBase | SQL) { + 117 0 : if (is(table, Subquery)) return table._.alias + 118 0 : if (is(table, SQLiteViewBase)) return getViewSelectedFieldsRuntime(table).name + 119 0 : if (is(table, SQL)) return undefined + 120 0 : return (table as unknown as Record<symbol, string | boolean>)[ + 121 0 : (table as unknown as Record<symbol, string | boolean>)[TableSymbol.IsAlias] + 122 0 : ? TableSymbol.Name + 123 : : TableSymbol.BaseName + 124 : ] as string + 125 : } + 126 : + 127 : export type { JoinNullability } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| drizzle-utils.ts | +
+ |
+ 67.4 % | +89 | +60 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 : import type * as Effect from "effect/Effect" + 3 : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" + 4 : import { entityKind } from "drizzle-orm/entity" + 5 1 : import { SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql" + 6 : import type { SQLiteTable } from "drizzle-orm/sqlite-core/table" + 7 : import type { SQLiteView } from "drizzle-orm/sqlite-core/view" + 8 : import type { SQLiteEffectSession } from "./session" + 9 : + 10 0 : function buildSQLiteEmbeddedCount(source: SQLiteTable | SQLiteView | SQL | SQLWrapper, filters?: SQL<unknown>) { + 11 90 : return sql<number>`(select count(*) from ${source}${sql.raw(" where ").if(filters)}${filters})` + 12 : } + 13 : + 14 0 : function buildSQLiteCount(source: SQLiteTable | SQLiteView | SQL | SQLWrapper, filters?: SQL<unknown>) { + 15 89 : return sql<number>`select count(*) from ${source}${sql.raw(" where ").if(filters)}${filters}` + 16 : } + 17 : + 18 : export interface SQLiteEffectCountBuilder<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase> + 19 : extends SQL<number>, + 20 : SQLWrapper<number>, + 21 : Effect.Effect<number, TEffectHKT["error"], TEffectHKT["context"]> {} + 22 : + 23 120 : export class SQLiteEffectCountBuilder<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase> extends SQL<number> { + 24 86 : static override readonly [entityKind]: string = "SQLiteEffectCountBuilder" + 25 : + 26 12 : private sql: SQL<number> + 27 19 : private session: SQLiteEffectSession<TEffectHKT, any, any> + 28 : + 29 0 : constructor(params: { + 30 0 : source: SQLiteTable | SQLiteView | SQL | SQLWrapper + 31 0 : filters?: SQL<unknown> + 32 0 : session: SQLiteEffectSession<TEffectHKT, any, any> + 33 0 : }) { + 34 0 : super(buildSQLiteEmbeddedCount(params.source, params.filters).queryChunks) + 35 0 : + 36 0 : this.session = params.session + 37 65 : this.sql = buildSQLiteCount(params.source, params.filters) + 38 : } + 39 : + 40 0 : execute(placeholderValues?: Record<string, unknown>) { + 41 0 : return this.session + 42 0 : .prepareQuery<{ + 43 0 : type: "async" + 44 0 : execute: number + 45 0 : run: unknown + 46 0 : all: unknown + 47 0 : get: unknown + 48 0 : values: unknown + 49 0 : }>(this.session.dialect.sqlToQuery(this.sql), undefined, "all", (rows) => { + 50 0 : const v = rows[0]?.[0] + 51 0 : if (typeof v === "number") return v + 52 0 : return v ? Number(v) : 0 + 53 0 : }) + 54 32 : .execute(placeholderValues) + 55 : } + 56 2 : } + 57 : + 58 89 : applyEffectWrapper(SQLiteEffectCountBuilder) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 : import { Effect } from "effect" + 3 : import type { SqlError } from "effect/unstable/sql/SqlError" + 4 : import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect" + 5 : import type { MutationOption } from "drizzle-orm/cache/core/cache" + 6 : import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" + 7 : import { entityKind } from "drizzle-orm/entity" + 8 : import type { TypedQueryBuilder } from "drizzle-orm/query-builders/query-builder" + 9 : import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations" + 10 : import { SelectionProxyHandler } from "drizzle-orm/selection-proxy" + 11 : import { type ColumnsSelection, type SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql" + 12 : import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect" + 13 : import { QueryBuilder } from "drizzle-orm/sqlite-core/query-builders/query-builder" + 14 : import type { SelectedFields } from "drizzle-orm/sqlite-core/query-builders/select.types" + 15 : import type { SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session" + 16 : import type { SQLiteTable } from "drizzle-orm/sqlite-core/table" + 17 : import type { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base" + 18 : import { WithSubquery } from "drizzle-orm/subquery" + 19 : import type { WithBuilder } from "drizzle-orm/sqlite-core/subquery" + 20 : import { SQLiteEffectCountBuilder } from "./count" + 21 : import { SQLiteEffectDeleteBase } from "./delete" + 22 : import { SQLiteEffectInsertBuilder } from "./insert" + 23 : import { SQLiteEffectRelationalQueryBuilder } from "./query" + 24 : import { SQLiteEffectRaw } from "./raw" + 25 : import { SQLiteEffectSelectBuilder } from "./select" + 26 : import type { SQLiteEffectSelectBase } from "./select" + 27 : import type { SQLiteEffectSession, SQLiteEffectTransaction } from "./session" + 28 : import { SQLiteEffectUpdateBuilder } from "./update" + 29 : + 30 : export class SQLiteEffectDatabase< + 31 : TEffectHKT extends QueryEffectHKTBase, + 32 : TRunResult, + 33 : TRelations extends AnyRelations = EmptyRelations, + 34 : > { + 35 : static readonly [entityKind]: string = "SQLiteEffectDatabase" + 36 : + 37 : declare readonly _: { + 38 : readonly relations: TRelations + 39 : readonly session: SQLiteEffectSession<TEffectHKT, TRunResult, TRelations> + 40 : } + 41 : + 42 1 : query: { + 43 : [K in keyof TRelations]: SQLiteEffectRelationalQueryBuilder<TRelations, TRelations[K], TEffectHKT> + 44 : } + 45 : + 46 13 : constructor( + 47 : /** @internal */ + 48 72 : readonly dialect: SQLiteAsyncDialect, + 49 : /** @internal */ + 50 72 : readonly session: SQLiteEffectSession<TEffectHKT, TRunResult, TRelations>, + 51 22 : relations: TRelations, + 52 90 : readonly rowModeRQB?: boolean, + 53 96 : readonly forbidJsonb?: boolean, + 54 10 : ) { + 55 32 : this._ = { + 56 32 : relations, + 57 22 : session, + 58 12 : } + 59 : + 60 40 : this.query = {} as (typeof this)["query"] + 61 132 : for (const [tableName, relation] of Object.entries(relations)) { + 62 24 : ;(this.query as SQLiteEffectDatabase<TEffectHKT, TRunResult, AnyRelations>["query"])[tableName] = + 63 39 : new SQLiteEffectRelationalQueryBuilder( + 64 11 : relations, + 65 32 : relations[relation.name]!.table as SQLiteTable, + 66 10 : relation, + 67 9 : dialect, + 68 9 : session, + 69 12 : rowModeRQB, + 70 11 : forbidJsonb, + 71 6 : ) + 72 9 : } + 73 : + 74 42 : this.$cache = { + 75 55 : invalidate: (_params: MutationOption) => Effect.void, + 76 13 : } + 77 : } + 78 : + 79 0 : $with: WithBuilder = (alias: string, selection?: ColumnsSelection) => { + 80 0 : const self = this + 81 0 : const as = ( + 82 0 : qb: + 83 0 : | TypedQueryBuilder<ColumnsSelection | undefined> + 84 0 : | SQL + 85 0 : | ((qb: QueryBuilder) => TypedQueryBuilder<ColumnsSelection | undefined> | SQL), + 86 0 : ) => { + 87 0 : if (typeof qb === "function") { + 88 0 : qb = qb(new QueryBuilder(self.dialect)) + 89 0 : } + 90 0 : + 91 0 : return new Proxy( + 92 0 : new WithSubquery( + 93 0 : qb.getSQL(), + 94 0 : selection ?? + 95 0 : (("getSelectedFields" in qb + 96 0 : ? ((qb as { getSelectedFields(): SelectedFields | undefined }).getSelectedFields() ?? {}) + 97 0 : : {}) as SelectedFields), + 98 0 : alias, + 99 0 : true, + 100 0 : ), + 101 0 : new SelectionProxyHandler({ alias, sqlAliasedBehavior: "alias", sqlBehavior: "error" }), + 102 0 : ) + 103 0 : } + 104 23 : return { as } + 105 : } + 106 : + 107 17 : $cache: { invalidate: EffectCacheShape["onMutate"] } + 108 : + 109 0 : $count(source: SQLiteTable | SQLiteViewBase | SQL | SQLWrapper, filters?: SQL<unknown>) { + 110 86 : return new SQLiteEffectCountBuilder({ source, filters, session: this.session }) + 111 : } + 112 : + 113 0 : with(...queries: WithSubquery[]) { + 114 0 : const self = this + 115 0 : + 116 0 : function select(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT> + 117 0 : function select<TSelection extends SelectedFields>( + 118 0 : fields: TSelection, + 119 0 : ): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT> + 120 0 : function select( + 121 0 : fields?: SelectedFields, + 122 0 : ): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> { + 123 0 : return new SQLiteEffectSelectBuilder({ + 124 0 : fields: fields ?? undefined, + 125 0 : session: self.session, + 126 0 : dialect: self.dialect, + 127 0 : withList: queries, + 128 0 : }) + 129 0 : } + 130 0 : + 131 0 : function selectDistinct(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT> + 132 0 : function selectDistinct<TSelection extends SelectedFields>( + 133 0 : fields: TSelection, + 134 0 : ): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT> + 135 0 : function selectDistinct( + 136 0 : fields?: SelectedFields, + 137 0 : ): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> { + 138 0 : return new SQLiteEffectSelectBuilder({ + 139 0 : fields: fields ?? undefined, + 140 0 : session: self.session, + 141 0 : dialect: self.dialect, + 142 0 : withList: queries, + 143 0 : distinct: true, + 144 0 : }) + 145 0 : } + 146 0 : + 147 0 : function update<TTable extends SQLiteTable>( + 148 0 : table: TTable, + 149 0 : ): SQLiteEffectUpdateBuilder<TTable, TRunResult, TEffectHKT> { + 150 0 : return new SQLiteEffectUpdateBuilder(table, self.session, self.dialect, queries) + 151 0 : } + 152 0 : + 153 0 : function insert<TTable extends SQLiteTable>( + 154 0 : into: TTable, + 155 0 : ): SQLiteEffectInsertBuilder<TTable, TRunResult, TEffectHKT> { + 156 0 : return new SQLiteEffectInsertBuilder(into, self.session, self.dialect, queries) + 157 0 : } + 158 0 : + 159 0 : function delete_<TTable extends SQLiteTable>( + 160 0 : from: TTable, + 161 0 : ): SQLiteEffectDeleteBase<TTable, TRunResult, undefined, false, never, TEffectHKT> { + 162 0 : return new SQLiteEffectDeleteBase(from, self.session, self.dialect, queries) + 163 0 : } + 164 0 : + 165 73 : return { select, selectDistinct, update, insert, delete: delete_ } + 166 : } + 167 : + 168 : select(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT> + 169 : select<TSelection extends SelectedFields>( + 170 : fields: TSelection, + 171 : ): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT> + 172 34 : select(fields?: SelectedFields): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> { + 173 237 : return new SQLiteEffectSelectBuilder({ fields: fields ?? undefined, session: this.session, dialect: this.dialect }) + 174 : } + 175 : + 176 : selectDistinct(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT> + 177 : selectDistinct<TSelection extends SelectedFields>( + 178 : fields: TSelection, + 179 : ): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT> + 180 0 : selectDistinct( + 181 0 : fields?: SelectedFields, + 182 0 : ): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> { + 183 0 : return new SQLiteEffectSelectBuilder({ + 184 0 : fields: fields ?? undefined, + 185 0 : session: this.session, + 186 0 : dialect: this.dialect, + 187 0 : distinct: true, + 188 9 : }) + 189 : } + 190 : + 191 32 : update<TTable extends SQLiteTable>(table: TTable): SQLiteEffectUpdateBuilder<TTable, TRunResult, TEffectHKT> { + 192 149 : return new SQLiteEffectUpdateBuilder(table, this.session, this.dialect) + 193 : } + 194 : + 195 30 : insert<TTable extends SQLiteTable>(into: TTable): SQLiteEffectInsertBuilder<TTable, TRunResult, TEffectHKT> { + 196 147 : return new SQLiteEffectInsertBuilder(into, this.session, this.dialect) + 197 : } + 198 : + 199 8 : delete<TTable extends SQLiteTable>( + 200 12 : from: TTable, + 201 10 : ): SQLiteEffectDeleteBase<TTable, TRunResult, undefined, false, never, TEffectHKT> { + 202 141 : return new SQLiteEffectDeleteBase(from, this.session, this.dialect) + 203 : } + 204 : + 205 5 : private raw<TResult>( + 206 14 : query: SQLWrapper | string, + 207 16 : action: "all" | "get" | "run" | "values", + 208 18 : execute: (query: SQL) => Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>, + 209 10 : ): SQLiteEffectRaw<TResult, TEffectHKT> { + 210 155 : const sequel = typeof query === "string" ? sql.raw(query) : query.getSQL() + 211 53 : return new SQLiteEffectRaw( + 212 44 : () => execute(sequel), + 213 16 : () => sequel, + 214 16 : action, + 215 27 : this.dialect, + 216 18 : (result) => result, + 217 9 : ) + 218 : } + 219 : + 220 29 : run(query: SQLWrapper | string): SQLiteEffectRaw<TRunResult, TEffectHKT> { + 221 139 : return this.raw(query, "run", (sequel) => this.session.run(sequel)) + 222 : } + 223 : + 224 29 : all<T = unknown>(query: SQLWrapper | string): SQLiteEffectRaw<T[], TEffectHKT> { + 225 139 : return this.raw(query, "all", (sequel) => this.session.all(sequel)) + 226 : } + 227 : + 228 29 : get<T = unknown>(query: SQLWrapper | string): SQLiteEffectRaw<T | undefined, TEffectHKT> { + 229 139 : return this.raw(query, "get", (sequel) => this.session.get(sequel)) + 230 : } + 231 : + 232 0 : values<T extends unknown[] = unknown[]>(query: SQLWrapper | string): SQLiteEffectRaw<T[], TEffectHKT> { + 233 81 : return this.raw(query, "values", (sequel) => this.session.values(sequel)) + 234 : } + 235 : + 236 27 : transaction: <A, E, R>( + 237 : transaction: (tx: SQLiteEffectTransaction<TEffectHKT, TRunResult, TRelations>) => Effect.Effect<A, E, R>, + 238 : config?: SQLiteTransactionConfig, + 239 107 : ) => Effect.Effect<A, E | SqlError, R> = (tx, config) => this.session.transaction(tx, config) + 240 2 : } + 241 : + 242 : export type SQLiteEffectWithReplicas<Q> = Q & { $primary: Q; $replicas: Q[] } + 243 : + 244 0 : export const withReplicas = < + 245 0 : TEffectHKT extends QueryEffectHKTBase, + 246 0 : TRunResult, + 247 0 : TRelations extends AnyRelations, + 248 0 : Q extends SQLiteEffectDatabase<TEffectHKT, TRunResult, TRelations>, + 249 0 : >( + 250 0 : primary: Q, + 251 0 : replicas: [Q, ...Q[]], + 252 0 : getReplica: (replicas: Q[]) => Q = () => replicas[Math.floor(Math.random() * replicas.length)]!, + 253 0 : ): SQLiteEffectWithReplicas<Q> => { + 254 0 : const select: Q["select"] = (...args: []) => getReplica(replicas).select(...args) + 255 0 : const selectDistinct: Q["selectDistinct"] = (...args: []) => getReplica(replicas).selectDistinct(...args) + 256 0 : const $count: Q["$count"] = (...args: [any]) => getReplica(replicas).$count(...args) + 257 0 : const _with: Q["with"] = (...args: []) => getReplica(replicas).with(...args) + 258 0 : const $with = ((...args: [string] | [string, ColumnsSelection]) => + 259 0 : args.length === 1 + 260 0 : ? getReplica(replicas).$with(args[0]) + 261 0 : : getReplica(replicas).$with(args[0], args[1])) as Q["$with"] + 262 0 : + 263 0 : const update: Q["update"] = (...args: [any]) => primary.update(...args) + 264 0 : const insert: Q["insert"] = (...args: [any]) => primary.insert(...args) + 265 0 : const $delete: Q["delete"] = (...args: [any]) => primary.delete(...args) + 266 0 : const run: Q["run"] = (...args: [any]) => primary.run(...args) + 267 0 : const all: Q["all"] = (...args: [any]) => primary.all(...args) + 268 0 : const get: Q["get"] = (...args: [any]) => primary.get(...args) + 269 0 : const values: Q["values"] = (...args: [any]) => primary.values(...args) + 270 0 : const transaction: Q["transaction"] = (...args: [any]) => primary.transaction(...args) + 271 0 : + 272 0 : return { + 273 0 : ...primary, + 274 0 : update, + 275 0 : insert, + 276 0 : delete: $delete, + 277 0 : run, + 278 0 : all, + 279 0 : get, + 280 0 : values, + 281 0 : transaction, + 282 0 : $primary: primary, + 283 0 : $replicas: replicas, + 284 0 : select, + 285 0 : selectDistinct, + 286 0 : $count, + 287 0 : $with, + 288 0 : with: _with, + 289 0 : get query() { + 290 0 : return getReplica(replicas).query + 291 0 : }, + 292 6 : } + 293 : } + 294 : + 295 : export type AnySQLiteEffectDatabase = SQLiteEffectDatabase<any, any, any> + 296 : export type AnySQLiteEffectSelectBase = SQLiteEffectSelectBase<any, any, any, any, any, any, any, any, any, any> ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 : import type * as Effect from "effect/Effect" + 3 : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" + 4 : import { entityKind } from "drizzle-orm/entity" + 5 : import type { SelectResultFields } from "drizzle-orm/query-builders/select.types" + 6 : import type { RunnableQuery } from "drizzle-orm/runnable-query" + 7 : import { SelectionProxyHandler } from "drizzle-orm/selection-proxy" + 8 : import type { Placeholder, Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql" + 9 : import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect" + 10 : import type { SQLiteDeleteConfig } from "drizzle-orm/sqlite-core/query-builders/delete" + 11 : import type { SelectedFieldsFlat } from "drizzle-orm/sqlite-core/query-builders/select.types" + 12 : import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session" + 13 : import { SQLiteTable } from "drizzle-orm/sqlite-core/table" + 14 : import { extractUsedTable } from "drizzle-orm/sqlite-core/utils" + 15 : import type { Subquery } from "drizzle-orm/subquery" + 16 : import { type DrizzleTypeError, type ValueOrArray } from "drizzle-orm/utils" + 17 : import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common" + 18 : import { getTableColumnsRuntime, orderSelectedFields } from "../../internal/drizzle-utils" + 19 : import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session" + 20 : + 21 : export type SQLiteEffectDeleteWithout< + 22 : T extends AnySQLiteEffectDelete, + 23 : TDynamic extends boolean, + 24 : K extends keyof T & string, + 25 : > = TDynamic extends true + 26 : ? T + 27 : : Omit< + 28 : SQLiteEffectDeleteBase< + 29 : T["_"]["table"], + 30 : T["_"]["runResult"], + 31 : T["_"]["returning"], + 32 : TDynamic, + 33 : T["_"]["excludedMethods"] | K, + 34 : T["_"]["effectHKT"] + 35 : >, + 36 : T["_"]["excludedMethods"] | K + 37 : > + 38 : + 39 : export type SQLiteEffectDeleteReturningAll< + 40 : T extends AnySQLiteEffectDelete, + 41 : TDynamic extends boolean, + 42 : > = SQLiteEffectDeleteWithout< + 43 : SQLiteEffectDeleteBase< + 44 : T["_"]["table"], + 45 : T["_"]["runResult"], + 46 : T["_"]["table"]["$inferSelect"], + 47 : T["_"]["dynamic"], + 48 : T["_"]["excludedMethods"], + 49 : T["_"]["effectHKT"] + 50 : >, + 51 : TDynamic, + 52 : "returning" + 53 : > + 54 : + 55 : export type SQLiteEffectDeleteReturning< + 56 : T extends AnySQLiteEffectDelete, + 57 : TDynamic extends boolean, + 58 : TSelectedFields extends SelectedFieldsFlat, + 59 : > = SQLiteEffectDeleteWithout< + 60 : SQLiteEffectDeleteBase< + 61 : T["_"]["table"], + 62 : T["_"]["runResult"], + 63 : SelectResultFields<TSelectedFields>, + 64 : T["_"]["dynamic"], + 65 : T["_"]["excludedMethods"], + 66 : T["_"]["effectHKT"] + 67 : >, + 68 : TDynamic, + 69 : "returning" + 70 : > + 71 : + 72 : export type SQLiteEffectDeleteExecute<T extends AnySQLiteEffectDelete> = T["_"]["returning"] extends undefined + 73 : ? T["_"]["runResult"] + 74 : : T["_"]["returning"][] + 75 : + 76 : export type SQLiteEffectDeletePrepare< + 77 : T extends AnySQLiteEffectDelete, + 78 : TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"], + 79 : > = SQLiteEffectPreparedQuery< + 80 : PreparedQueryConfig & { + 81 : run: T["_"]["runResult"] + 82 : all: T["_"]["returning"] extends undefined + 83 : ? DrizzleTypeError<".all() cannot be used without .returning()"> + 84 : : T["_"]["returning"][] + 85 : get: T["_"]["returning"] extends undefined + 86 : ? DrizzleTypeError<".get() cannot be used without .returning()"> + 87 : : T["_"]["returning"] | undefined + 88 : values: T["_"]["returning"] extends undefined + 89 : ? DrizzleTypeError<".values() cannot be used without .returning()"> + 90 : : any[][] + 91 : execute: SQLiteEffectDeleteExecute<T> + 92 : }, + 93 : TEffectHKT + 94 : > + 95 : + 96 : export type SQLiteEffectDeleteDynamic<T extends AnySQLiteEffectDelete> = SQLiteEffectDelete< + 97 : T["_"]["table"], + 98 : T["_"]["runResult"], + 99 : T["_"]["returning"], + 100 : T["_"]["effectHKT"] + 101 : > + 102 : + 103 : export type SQLiteEffectDelete< + 104 : TTable extends SQLiteTable = SQLiteTable, + 105 : TRunResult = unknown, + 106 : TReturning extends Record<string, unknown> | undefined = undefined, + 107 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 108 : > = SQLiteEffectDeleteBase<TTable, TRunResult, TReturning, true, never, TEffectHKT> + 109 : + 110 : export type AnySQLiteEffectDelete = SQLiteEffectDeleteBase<any, any, any, any, any, any> + 111 : + 112 : export interface SQLiteEffectDeleteBase< + 113 : TTable extends SQLiteTable, + 114 : TRunResult, + 115 : TReturning extends Record<string, unknown> | undefined = undefined, + 116 : TDynamic extends boolean = false, + 117 : _TExcludedMethods extends string = never, + 118 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 119 : > extends RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, + 120 : SQLWrapper, + 121 : Effect.Effect< + 122 : TReturning extends undefined ? TRunResult : TReturning[], + 123 : TEffectHKT["error"], + 124 : TEffectHKT["context"] + 125 : > { + 126 : readonly _: { + 127 : dialect: "sqlite" + 128 : readonly table: TTable + 129 : readonly resultType: "async" + 130 : readonly runResult: TRunResult + 131 : readonly returning: TReturning + 132 : readonly dynamic: TDynamic + 133 : readonly excludedMethods: _TExcludedMethods + 134 : readonly result: TReturning extends undefined ? TRunResult : TReturning[] + 135 : readonly effectHKT: TEffectHKT + 136 : } + 137 : } + 138 : + 139 : export class SQLiteEffectDeleteBase< + 140 : TTable extends SQLiteTable, + 141 : TRunResult, + 142 : TReturning extends Record<string, unknown> | undefined = undefined, + 143 : TDynamic extends boolean = false, + 144 : _TExcludedMethods extends string = never, + 145 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 146 : > + 147 : implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper + 148 : { + 149 : static readonly [entityKind]: string = "SQLiteEffectDelete" + 150 : + 151 : /** @internal */ + 152 1 : config: SQLiteDeleteConfig + 153 : + 154 13 : constructor( + 155 60 : private table: TTable, + 156 108 : private effectSession: SQLiteEffectSession<TEffectHKT, TRunResult, any>, + 157 108 : private effectDialect: SQLiteDialect, + 158 20 : withList?: Subquery[], + 159 10 : ) { + 160 76 : this.config = { table, withList } + 161 : } + 162 : + 163 31 : where(where: SQL | undefined): SQLiteEffectDeleteWithout<this, TDynamic, "where"> { + 164 60 : this.config.where = where + 165 29 : return this as any + 166 : } + 167 : + 168 : orderBy( + 169 : builder: (deleteTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>, + 170 : ): SQLiteEffectDeleteWithout<this, TDynamic, "orderBy"> + 171 : orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): SQLiteEffectDeleteWithout<this, TDynamic, "orderBy"> + 172 0 : orderBy( + 173 0 : ...columns: + 174 0 : | [(deleteTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>] + 175 0 : | (SQLiteColumn | SQL | SQL.Aliased)[] + 176 0 : ): SQLiteEffectDeleteWithout<this, TDynamic, "orderBy"> { + 177 0 : if (typeof columns[0] === "function") { + 178 0 : const orderBy = columns[0]( + 179 0 : new Proxy( + 180 0 : getTableColumnsRuntime(this.config.table), + 181 0 : new SelectionProxyHandler({ sqlAliasedBehavior: "alias", sqlBehavior: "sql" }), + 182 0 : ) as any, + 183 0 : ) + 184 0 : + 185 0 : this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy] + 186 0 : return this as any + 187 0 : } + 188 0 : + 189 0 : this.config.orderBy = columns as (SQLiteColumn | SQL | SQL.Aliased)[] + 190 18 : return this as any + 191 : } + 192 : + 193 0 : limit(limit: number | Placeholder): SQLiteEffectDeleteWithout<this, TDynamic, "limit"> { + 194 0 : this.config.limit = limit + 195 18 : return this as any + 196 : } + 197 : + 198 : returning(): SQLiteEffectDeleteReturningAll<this, TDynamic> + 199 : returning<TSelectedFields extends SelectedFieldsFlat>( + 200 : fields: TSelectedFields, + 201 : ): SQLiteEffectDeleteReturning<this, TDynamic, TSelectedFields> + 202 11 : returning( + 203 90 : fields: SelectedFieldsFlat = getTableColumnsRuntime(this.table), + 204 10 : ): SQLiteEffectDeleteReturning<this, TDynamic, any> | SQLiteEffectDeleteReturningAll<this, TDynamic> { + 205 112 : this.config.returning = orderSelectedFields<SQLiteColumn>(fields) + 206 29 : return this as any + 207 : } + 208 : + 209 : /** @internal */ + 210 22 : getSQL(): SQL { + 211 117 : return this.effectDialect.buildDeleteQuery(this.config) + 212 : } + 213 : + 214 0 : toSQL(): Query { + 215 58 : return this.effectDialect.sqlToQuery(this.getSQL()) + 216 : } + 217 : + 218 : /** @internal */ + 219 66 : _prepare(isOneTimeQuery = true): SQLiteEffectDeletePrepare<this, TEffectHKT> { + 220 147 : return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"]( + 221 92 : this.effectDialect.sqlToQuery(this.getSQL()), + 222 46 : this.config.returning, + 223 75 : this.config.returning ? "all" : "run", + 224 22 : undefined, + 225 14 : { + 226 42 : type: "delete", + 227 94 : tables: extractUsedTable(this.config.table), + 228 2 : }, + 229 9 : ) as SQLiteEffectDeletePrepare<this, TEffectHKT> + 230 : } + 231 : + 232 0 : prepare(): SQLiteEffectDeletePrepare<this, TEffectHKT> { + 233 35 : return this._prepare(false) + 234 : } + 235 : + 236 67 : run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => { + 237 100 : return this._prepare().run(placeholderValues) + 238 : } + 239 : + 240 0 : all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => { + 241 55 : return this._prepare().all(placeholderValues) + 242 : } + 243 : + 244 67 : get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => { + 245 100 : return this._prepare().get(placeholderValues) + 246 : } + 247 : + 248 0 : values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => { + 249 58 : return this._prepare().values(placeholderValues) + 250 : } + 251 : + 252 0 : execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => { + 253 58 : return this._prepare().execute(placeholderValues) + 254 : } + 255 : + 256 0 : $dynamic(): SQLiteEffectDeleteDynamic<this> { + 257 17 : return this as any + 258 : } + 259 2 : } + 260 : + 261 85 : applyEffectWrapper(SQLiteEffectDeleteBase) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| count.ts | +
+ |
+ 31.4 % | +35 | +11 | +|
| db.ts | +
+ |
+ 33.2 % | +202 | +67 | +|
| delete.ts | +
+ |
+ 62.3 % | +69 | +43 | +|
| insert.ts | +
+ |
+ 83.1 % | +118 | +98 | +|
| query.ts | +
+ |
+ 15.5 % | +129 | +20 | +|
| raw.ts | +
+ |
+ 81.2 % | +16 | +13 | +|
| select.ts | +
+ |
+ 87.1 % | +101 | +88 | +|
| session.ts | +
+ |
+ 67.9 % | +308 | +209 | +|
| update.ts | +
+ |
+ 54.4 % | +136 | +74 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| query.ts | +
+ |
+ 15.5 % | +129 | +20 | +|
| count.ts | +
+ |
+ 31.4 % | +35 | +11 | +|
| db.ts | +
+ |
+ 33.2 % | +202 | +67 | +|
| update.ts | +
+ |
+ 54.4 % | +136 | +74 | +|
| delete.ts | +
+ |
+ 62.3 % | +69 | +43 | +|
| session.ts | +
+ |
+ 67.9 % | +308 | +209 | +|
| raw.ts | +
+ |
+ 81.2 % | +16 | +13 | +|
| insert.ts | +
+ |
+ 83.1 % | +118 | +98 | +|
| select.ts | +
+ |
+ 87.1 % | +101 | +88 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| count.ts | +
+ |
+ 31.4 % | +35 | +11 | +|
| db.ts | +
+ |
+ 33.2 % | +202 | +67 | +|
| delete.ts | +
+ |
+ 62.3 % | +69 | +43 | +|
| insert.ts | +
+ |
+ 83.1 % | +118 | +98 | +|
| query.ts | +
+ |
+ 15.5 % | +129 | +20 | +|
| raw.ts | +
+ |
+ 81.2 % | +16 | +13 | +|
| select.ts | +
+ |
+ 87.1 % | +101 | +88 | +|
| session.ts | +
+ |
+ 67.9 % | +308 | +209 | +|
| update.ts | +
+ |
+ 54.4 % | +136 | +74 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 : import type * as Effect from "effect/Effect" + 3 : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" + 4 : import { entityKind, is } from "drizzle-orm/entity" + 5 : import type { SelectResultFields } from "drizzle-orm/query-builders/select.types" + 6 : import type { RunnableQuery } from "drizzle-orm/runnable-query" + 7 : import type { Query, SQLWrapper } from "drizzle-orm/sql/sql" + 8 : import { Param, SQL, sql } from "drizzle-orm/sql/sql" + 9 : import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect" + 10 : import type { IndexColumn } from "drizzle-orm/sqlite-core/indexes" + 11 : import type { + 12 : SQLiteInsertConfig, + 13 : SQLiteInsertSelectQueryBuilder, + 14 : SQLiteInsertValue, + 15 : } from "drizzle-orm/sqlite-core/query-builders/insert" + 16 : import type { SelectedFieldsFlat } from "drizzle-orm/sqlite-core/query-builders/select.types" + 17 : import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session" + 18 : import { SQLiteTable } from "drizzle-orm/sqlite-core/table" + 19 : import { extractUsedTable } from "drizzle-orm/sqlite-core/utils" + 20 : import type { Subquery } from "drizzle-orm/subquery" + 21 : import { type DrizzleTypeError, haveSameKeys } from "drizzle-orm/utils" + 22 : import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common" + 23 : import { QueryBuilder } from "drizzle-orm/sqlite-core/query-builders/query-builder" + 24 : import type { SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update" + 25 : import { getTableColumnsRuntime, mapUpdateSet, orderSelectedFields } from "../../internal/drizzle-utils" + 26 : import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session" + 27 : + 28 : export type SQLiteEffectInsertWithout< + 29 : T extends AnySQLiteEffectInsert, + 30 : TDynamic extends boolean, + 31 : K extends keyof T & string, + 32 : > = TDynamic extends true + 33 : ? T + 34 : : Omit< + 35 : SQLiteEffectInsertBase< + 36 : T["_"]["table"], + 37 : T["_"]["runResult"], + 38 : T["_"]["returning"], + 39 : TDynamic, + 40 : T["_"]["excludedMethods"] | K, + 41 : T["_"]["effectHKT"] + 42 : >, + 43 : T["_"]["excludedMethods"] | K + 44 : > + 45 : + 46 : export type SQLiteEffectInsertReturning< + 47 : T extends AnySQLiteEffectInsert, + 48 : TDynamic extends boolean, + 49 : TSelectedFields extends SelectedFieldsFlat, + 50 : > = SQLiteEffectInsertWithout< + 51 : SQLiteEffectInsertBase< + 52 : T["_"]["table"], + 53 : T["_"]["runResult"], + 54 : SelectResultFields<TSelectedFields>, + 55 : TDynamic, + 56 : T["_"]["excludedMethods"], + 57 : T["_"]["effectHKT"] + 58 : >, + 59 : TDynamic, + 60 : "returning" + 61 : > + 62 : + 63 : export type SQLiteEffectInsertReturningAll< + 64 : T extends AnySQLiteEffectInsert, + 65 : TDynamic extends boolean, + 66 : > = SQLiteEffectInsertWithout< + 67 : SQLiteEffectInsertBase< + 68 : T["_"]["table"], + 69 : T["_"]["runResult"], + 70 : T["_"]["table"]["$inferSelect"], + 71 : TDynamic, + 72 : T["_"]["excludedMethods"], + 73 : T["_"]["effectHKT"] + 74 : >, + 75 : TDynamic, + 76 : "returning" + 77 : > + 78 : + 79 : export type SQLiteEffectInsertDynamic<T extends AnySQLiteEffectInsert> = SQLiteEffectInsert< + 80 : T["_"]["table"], + 81 : T["_"]["runResult"], + 82 : T["_"]["returning"], + 83 : T["_"]["effectHKT"] + 84 : > + 85 : + 86 : export type SQLiteEffectInsertOnConflictDoUpdateConfig<T extends AnySQLiteEffectInsert> = { + 87 : target: IndexColumn | IndexColumn[] + 88 : /** @deprecated - use either `targetWhere` or `setWhere` */ + 89 : where?: SQL + 90 : targetWhere?: SQL + 91 : setWhere?: SQL + 92 : set: SQLiteUpdateSetSource<T["_"]["table"]> + 93 : } + 94 : + 95 : export type SQLiteEffectInsertExecute<T extends AnySQLiteEffectInsert> = T["_"]["returning"] extends undefined + 96 : ? T["_"]["runResult"] + 97 : : T["_"]["returning"][] + 98 : + 99 : export type SQLiteEffectInsertPrepare< + 100 : T extends AnySQLiteEffectInsert, + 101 : TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"], + 102 : > = SQLiteEffectPreparedQuery< + 103 : PreparedQueryConfig & { + 104 : run: T["_"]["runResult"] + 105 : all: T["_"]["returning"] extends undefined + 106 : ? DrizzleTypeError<".all() cannot be used without .returning()"> + 107 : : T["_"]["returning"][] + 108 : get: T["_"]["returning"] extends undefined + 109 : ? DrizzleTypeError<".get() cannot be used without .returning()"> + 110 : : T["_"]["returning"] + 111 : values: T["_"]["returning"] extends undefined + 112 : ? DrizzleTypeError<".values() cannot be used without .returning()"> + 113 : : any[][] + 114 : execute: SQLiteEffectInsertExecute<T> + 115 : }, + 116 : TEffectHKT + 117 : > + 118 : + 119 : export type SQLiteEffectInsert< + 120 : TTable extends SQLiteTable = SQLiteTable, + 121 : TRunResult = unknown, + 122 : TReturning = any, + 123 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 124 : > = SQLiteEffectInsertBase<TTable, TRunResult, TReturning, true, never, TEffectHKT> + 125 : + 126 : export type AnySQLiteEffectInsert = SQLiteEffectInsertBase<any, any, any, any, any, any> + 127 : + 128 : export class SQLiteEffectInsertBuilder< + 129 : TTable extends SQLiteTable, + 130 : TRunResult, + 131 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 132 : > { + 133 1 : static readonly [entityKind]: string = "SQLiteEffectInsertBuilder" + 134 : + 135 13 : constructor( + 136 60 : protected table: TTable, + 137 72 : protected session: SQLiteEffectSession<TEffectHKT, TRunResult, any>, + 138 72 : protected dialect: SQLiteDialect, + 139 78 : private withList?: Subquery[], + 140 10 : ) {} + 141 : + 142 : values( + 143 : value: SQLiteInsertValue<TTable>, + 144 : ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> + 145 : values( + 146 : values: SQLiteInsertValue<TTable>[], + 147 : ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> + 148 8 : values( + 149 16 : values: SQLiteInsertValue<TTable> | SQLiteInsertValue<TTable>[], + 150 10 : ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> { + 151 107 : values = Array.isArray(values) ? values : [values] + 152 56 : if (values.length === 0) { + 153 71 : throw new Error("values() must be called with at least one value") + 154 9 : } + 155 99 : const mappedValues = values.map((entry) => { + 156 48 : const result: Record<string, Param | SQL> = {} + 157 108 : const cols = getTableColumnsRuntime(this.table) + 158 99 : for (const colKey of Object.keys(entry)) { + 159 78 : const colValue = entry[colKey as keyof typeof entry] + 160 165 : result[colKey] = is(colValue, SQL) ? colValue : new Param(colValue, cols[colKey]) + 161 13 : } + 162 32 : return result + 163 12 : }) + 164 : + 165 211 : return new SQLiteEffectInsertBase(this.table, mappedValues, this.session, this.dialect, this.withList) + 166 : } + 167 : + 168 : select( + 169 : selectQuery: (qb: QueryBuilder) => SQLiteInsertSelectQueryBuilder<TTable>, + 170 : ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> + 171 : select( + 172 : selectQuery: (qb: QueryBuilder) => SQL, + 173 : ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> + 174 : select(selectQuery: SQL): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> + 175 : select( + 176 : selectQuery: SQLiteInsertSelectQueryBuilder<TTable>, + 177 : ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> + 178 0 : select( + 179 0 : selectQuery: + 180 0 : | SQL + 181 0 : | SQLiteInsertSelectQueryBuilder<TTable> + 182 0 : | ((qb: QueryBuilder) => SQLiteInsertSelectQueryBuilder<TTable> | SQL), + 183 0 : ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> { + 184 0 : const select = typeof selectQuery === "function" ? selectQuery(new QueryBuilder()) : selectQuery + 185 0 : + 186 0 : if (!is(select, SQL) && !haveSameKeys(getTableColumnsRuntime(this.table), select._.selectedFields)) { + 187 0 : throw new Error( + 188 0 : "Insert select error: selected fields are not the same or are in a different order compared to the table definition", + 189 0 : ) + 190 0 : } + 191 0 : + 192 108 : return new SQLiteEffectInsertBase(this.table, select, this.session, this.dialect, this.withList, true) + 193 : } + 194 2 : } + 195 : + 196 : export interface SQLiteEffectInsertBase< + 197 : TTable extends SQLiteTable, + 198 : TRunResult, + 199 : TReturning = undefined, + 200 : TDynamic extends boolean = false, + 201 : _TExcludedMethods extends string = never, + 202 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 203 : > extends SQLWrapper, + 204 : RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, + 205 : Effect.Effect< + 206 : TReturning extends undefined ? TRunResult : TReturning[], + 207 : TEffectHKT["error"], + 208 : TEffectHKT["context"] + 209 : > { + 210 : readonly _: { + 211 : readonly dialect: "sqlite" + 212 : readonly table: TTable + 213 : readonly resultType: "async" + 214 : readonly runResult: TRunResult + 215 : readonly returning: TReturning + 216 : readonly dynamic: TDynamic + 217 : readonly excludedMethods: _TExcludedMethods + 218 : readonly result: TReturning extends undefined ? TRunResult : TReturning[] + 219 : readonly effectHKT: TEffectHKT + 220 : } + 221 : } + 222 : + 223 70 : export class SQLiteEffectInsertBase< + 224 : TTable extends SQLiteTable, + 225 : TRunResult, + 226 : TReturning = undefined, + 227 : TDynamic extends boolean = false, + 228 : _TExcludedMethods extends string = never, + 229 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 230 : > + 231 : implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper + 232 6 : { + 233 74 : static readonly [entityKind]: string = "SQLiteEffectInsert" + 234 : + 235 : /** @internal */ + 236 17 : config: SQLiteInsertConfig<TTable> + 237 : + 238 13 : constructor( + 239 76 : private table: TTable, + 240 16 : values: SQLiteInsertConfig["values"], + 241 140 : private effectSession: SQLiteEffectSession<TEffectHKT, TRunResult, any>, + 242 156 : private effectDialect: SQLiteDialect, + 243 20 : withList?: Subquery[], + 244 16 : select?: boolean, + 245 10 : ) { + 246 108 : this.config = { table, values: values as any, withList, select } + 247 : } + 248 : + 249 : returning(): SQLiteEffectInsertReturningAll<this, TDynamic> + 250 : returning<TSelectedFields extends SelectedFieldsFlat>( + 251 : fields: TSelectedFields, + 252 : ): SQLiteEffectInsertReturning<this, TDynamic, TSelectedFields> + 253 11 : returning( + 254 104 : fields: SelectedFieldsFlat = getTableColumnsRuntime(this.config.table), + 255 10 : ): SQLiteEffectInsertWithout<AnySQLiteEffectInsert, TDynamic, "returning"> { + 256 112 : this.config.returning = orderSelectedFields<SQLiteColumn>(fields) + 257 29 : return this as any + 258 : } + 259 : + 260 57 : onConflictDoNothing(config: { target?: IndexColumn | IndexColumn[]; where?: SQL } = {}): this { + 261 130 : if (!this.config.onConflict) this.config.onConflict = [] + 262 : + 263 79 : if (config.target === undefined) { + 264 128 : this.config.onConflict.push(sql` on conflict do nothing`) + 265 27 : return this + 266 5 : } + 267 : + 268 101 : const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}` + 269 72 : const whereSql = config.where ? sql` where ${config.where}` : sql`` + 270 86 : this.config.onConflict.push(sql` on conflict ${targetSql} do nothing${whereSql}`) + 271 18 : return this + 272 : } + 273 : + 274 46 : onConflictDoUpdate(config: SQLiteEffectInsertOnConflictDoUpdateConfig<this>): this { + 275 128 : if (config.where && (config.targetWhere || config.setWhere)) { + 276 16 : throw new Error( + 277 141 : 'You cannot use both "where" and "targetWhere"/"setWhere" at the same time - "where" is deprecated, use "targetWhere" or "setWhere" instead.', + 278 6 : ) + 279 9 : } + 280 : + 281 130 : if (!this.config.onConflict) this.config.onConflict = [] + 282 : + 283 122 : const whereSql = config.where ? sql` where ${config.where}` : undefined + 284 152 : const targetWhereSql = config.targetWhere ? sql` where ${config.targetWhere}` : undefined + 285 167 : const setWhereSql = config.setWhere ? sql` where ${config.setWhere}` : undefined + 286 199 : const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}` + 287 98 : const setSql = this.effectDialect.buildUpdateSet( + 288 38 : this.config.table, + 289 86 : mapUpdateSet(this.config.table, config.set as SQLiteUpdateSetSource<TTable>), + 290 12 : ) + 291 56 : this.config.onConflict.push( + 292 192 : sql` on conflict ${targetSql}${targetWhereSql} do update set ${setSql}${whereSql}${setWhereSql}`, + 293 12 : ) + 294 29 : return this + 295 : } + 296 : + 297 : /** @internal */ + 298 22 : getSQL(): SQL { + 299 117 : return this.effectDialect.buildInsertQuery(this.config) + 300 : } + 301 : + 302 0 : toSQL(): Query { + 303 58 : return this.effectDialect.sqlToQuery(this.getSQL()) + 304 : } + 305 : + 306 : /** @internal */ + 307 66 : _prepare(isOneTimeQuery = true): SQLiteEffectInsertPrepare<this, TEffectHKT> { + 308 147 : return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"]( + 309 92 : this.effectDialect.sqlToQuery(this.getSQL()), + 310 46 : this.config.returning, + 311 75 : this.config.returning ? "all" : "run", + 312 22 : undefined, + 313 14 : { + 314 42 : type: "insert", + 315 94 : tables: extractUsedTable(this.config.table), + 316 2 : }, + 317 9 : ) as SQLiteEffectInsertPrepare<this, TEffectHKT> + 318 : } + 319 : + 320 0 : prepare(): SQLiteEffectInsertPrepare<this, TEffectHKT> { + 321 35 : return this._prepare(false) + 322 : } + 323 : + 324 67 : run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => { + 325 100 : return this._prepare().run(placeholderValues) + 326 : } + 327 : + 328 0 : all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => { + 329 55 : return this._prepare().all(placeholderValues) + 330 : } + 331 : + 332 67 : get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => { + 333 100 : return this._prepare().get(placeholderValues) + 334 : } + 335 : + 336 0 : values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => { + 337 58 : return this._prepare().values(placeholderValues) + 338 : } + 339 : + 340 0 : execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => { + 341 58 : return this._prepare().execute(placeholderValues) + 342 : } + 343 : + 344 0 : $dynamic(): SQLiteEffectInsertDynamic<this> { + 345 17 : return this as any + 346 : } + 347 2 : } + 348 : + 349 85 : applyEffectWrapper(SQLiteEffectInsertBase) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 : import type * as Effect from "effect/Effect" + 3 : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" + 4 : import { entityKind } from "drizzle-orm/entity" + 5 : import { + 6 : type BuildQueryResult, + 7 : type BuildRelationalQueryResult, + 8 : type DBQueryConfig, + 9 : makeDefaultRqbMapper, + 10 : type TableRelationalConfig, + 11 : type TablesRelationalConfig, + 12 : } from "drizzle-orm/relations" + 13 : import type { RunnableQuery } from "drizzle-orm/runnable-query" + 14 : import { type Query, type SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql" + 15 : import type { KnownKeysOnly } from "drizzle-orm/utils" + 16 : import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect" + 17 : import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session" + 18 : import type { SQLiteTable } from "drizzle-orm/sqlite-core/table" + 19 : import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session" + 20 : + 21 : export class SQLiteEffectRelationalQueryBuilder< + 22 : TSchema extends TablesRelationalConfig, + 23 : TFields extends TableRelationalConfig, + 24 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 25 : > { + 26 1 : static readonly [entityKind]: string = "SQLiteEffectRelationalQueryBuilderV2" + 27 : + 28 0 : constructor( + 29 0 : private schema: TSchema, + 30 0 : private table: SQLiteTable, + 31 0 : private tableConfig: TableRelationalConfig, + 32 0 : private dialect: SQLiteDialect, + 33 0 : private session: SQLiteEffectSession<TEffectHKT, any, any>, + 34 0 : private rowMode?: boolean, + 35 0 : private forbidJsonb?: boolean, + 36 5 : ) {} + 37 : + 38 0 : findMany<TConfig extends DBQueryConfig<"many", TSchema, TFields>>( + 39 0 : config?: KnownKeysOnly<TConfig, DBQueryConfig<"many", TSchema, TFields>>, + 40 0 : ): SQLiteEffectRelationalQuery<BuildQueryResult<TSchema, TFields, TConfig>[], TEffectHKT> { + 41 0 : return new SQLiteEffectRelationalQuery( + 42 0 : this.schema, + 43 0 : this.table, + 44 0 : this.tableConfig, + 45 0 : this.dialect, + 46 0 : this.session, + 47 0 : (config as DBQueryConfig<"many"> | undefined) ?? true, + 48 0 : "many", + 49 0 : this.rowMode, + 50 0 : this.forbidJsonb, + 51 8 : ) + 52 : } + 53 : + 54 0 : findFirst<TConfig extends DBQueryConfig<"one", TSchema, TFields>>( + 55 0 : config?: KnownKeysOnly<TConfig, DBQueryConfig<"one", TSchema, TFields>>, + 56 0 : ): SQLiteEffectRelationalQuery<BuildQueryResult<TSchema, TFields, TConfig> | undefined, TEffectHKT> { + 57 0 : return new SQLiteEffectRelationalQuery( + 58 0 : this.schema, + 59 0 : this.table, + 60 0 : this.tableConfig, + 61 0 : this.dialect, + 62 0 : this.session, + 63 0 : (config as DBQueryConfig<"one"> | undefined) ?? true, + 64 0 : "first", + 65 0 : this.rowMode, + 66 0 : this.forbidJsonb, + 67 7 : ) + 68 : } + 69 2 : } + 70 : + 71 : export interface SQLiteEffectRelationalQuery<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase> + 72 : extends Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>, + 73 : RunnableQuery<TResult, "sqlite">, + 74 : SQLWrapper {} + 75 : + 76 80 : export class SQLiteEffectRelationalQuery<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase> + 77 : implements RunnableQuery<TResult, "sqlite">, SQLWrapper + 78 6 : { + 79 96 : static readonly [entityKind]: string = "SQLiteEffectRelationalQueryV2" + 80 : + 81 : declare readonly _: { + 82 : readonly dialect: "sqlite" + 83 : readonly type: "async" + 84 : readonly result: TResult + 85 : } + 86 : + 87 : /** @internal */ + 88 14 : mode: "many" | "first" + 89 : /** @internal */ + 90 15 : table: SQLiteTable + 91 : + 92 0 : constructor( + 93 0 : private schema: TablesRelationalConfig, + 94 0 : table: SQLiteTable, + 95 0 : private tableConfig: TableRelationalConfig, + 96 0 : private dialect: SQLiteDialect, + 97 0 : private session: SQLiteEffectSession<TEffectHKT, any, any>, + 98 0 : private config: DBQueryConfig<"many" | "one"> | true, + 99 0 : mode: "many" | "first", + 100 0 : private rowMode?: boolean, + 101 0 : private forbidJsonb?: boolean, + 102 0 : ) { + 103 0 : this.mode = mode + 104 25 : this.table = table + 105 : } + 106 : + 107 : /** @internal */ + 108 0 : getSQL(): SQL { + 109 34 : return this._getQuery().sql + 110 : } + 111 : + 112 : /** @internal */ + 113 0 : _prepare( + 114 0 : isOneTimeQuery = true, + 115 0 : ): SQLiteEffectPreparedQuery< + 116 0 : PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult }, + 117 0 : TEffectHKT, + 118 0 : true + 119 0 : > { + 120 0 : const { query, builtQuery } = this._toSQL() + 121 0 : const mapperConfig = { + 122 0 : isFirst: this.mode === "first", + 123 0 : parseJson: !this.rowMode, + 124 0 : parseJsonIfString: false, + 125 0 : rootJsonMappers: true, + 126 0 : selection: query.selection, + 127 0 : } + 128 0 : + 129 0 : return this.session[isOneTimeQuery ? "prepareOneTimeRelationalQuery" : "prepareRelationalQuery"]( + 130 0 : builtQuery, + 131 0 : undefined, + 132 0 : this.mode === "first" ? "get" : "all", + 133 0 : makeDefaultRqbMapper(mapperConfig), + 134 0 : mapperConfig, + 135 8 : ) as SQLiteEffectPreparedQuery< + 136 : PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult }, + 137 : TEffectHKT, + 138 : true + 139 : > + 140 : } + 141 : + 142 0 : prepare(): SQLiteEffectPreparedQuery< + 143 0 : PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult }, + 144 0 : TEffectHKT, + 145 0 : true + 146 0 : > { + 147 34 : return this._prepare(false) + 148 : } + 149 : + 150 0 : private _getQuery() { + 151 0 : const jsonb = this.forbidJsonb ? sql`json` : sql`jsonb` + 152 0 : + 153 0 : const query = this.dialect.buildRelationalQuery({ + 154 0 : schema: this.schema, + 155 0 : table: this.table, + 156 0 : tableConfig: this.tableConfig, + 157 0 : queryConfig: this.config, + 158 0 : mode: this.mode, + 159 0 : isNested: this.rowMode, + 160 0 : jsonb, + 161 0 : }) + 162 0 : + 163 0 : if (this.rowMode) { + 164 0 : const jsonColumns = sql.join( + 165 0 : query.selection.map((s) => { + 166 0 : return sql`${sql.raw(this.dialect.escapeString(s.key))}, ${ + 167 0 : s.selection ? sql`${jsonb}(${sql.identifier(s.key)})` : sql.identifier(s.key) + 168 0 : }` + 169 0 : }), + 170 0 : sql`, `, + 171 0 : ) + 172 0 : + 173 0 : query.sql = sql`select json_object(${jsonColumns}) as ${sql.identifier("r")} from (${query.sql}) as ${sql.identifier( + 174 0 : "t", + 175 0 : )}` + 176 0 : } + 177 0 : + 178 19 : return query + 179 : } + 180 : + 181 0 : private _toSQL(): { query: BuildRelationalQueryResult; builtQuery: Query } { + 182 0 : const query = this._getQuery() + 183 0 : + 184 0 : const builtQuery = this.dialect.sqlToQuery(query.sql) + 185 0 : + 186 35 : return { query, builtQuery } + 187 : } + 188 : + 189 0 : toSQL(): Query { + 190 38 : return this._toSQL().builtQuery + 191 : } + 192 : + 193 0 : execute(placeholderValues?: Record<string, unknown>) { + 194 116 : return this.mode === "first" ? this._prepare().get(placeholderValues) : this._prepare().all(placeholderValues) + 195 : } + 196 2 : } + 197 : + 198 95 : applyEffectWrapper(SQLiteEffectRelationalQuery) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 : import type * as Effect from "effect/Effect" + 3 : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" + 4 : import { entityKind } from "drizzle-orm/entity" + 5 : import type { RunnableQuery } from "drizzle-orm/runnable-query" + 6 : import type { PreparedQuery } from "drizzle-orm/session" + 7 : import type { Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql" + 8 : import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect" + 9 : + 10 : type SQLiteEffectRawAction = "all" | "get" | "values" | "run" + 11 : + 12 : export interface SQLiteEffectRaw<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase> + 13 : extends Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>, + 14 : RunnableQuery<TResult, "sqlite">, + 15 : SQLWrapper {} + 16 : + 17 : export class SQLiteEffectRaw<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase> + 18 : implements RunnableQuery<TResult, "sqlite">, SQLWrapper, PreparedQuery + 19 : { + 20 1 : static readonly [entityKind]: string = "SQLiteEffectRaw" + 21 : + 22 : declare readonly _: { + 23 : readonly dialect: "sqlite" + 24 : readonly result: TResult + 25 : } + 26 : + 27 13 : constructor( + 28 72 : public execute: () => Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>, + 29 : /** @internal */ + 30 66 : public getSQL: () => SQL, + 31 66 : private action: SQLiteEffectRawAction, + 32 72 : private dialect: SQLiteAsyncDialect, + 33 114 : private mapBatchResult: (result: unknown) => unknown, + 34 10 : ) {} + 35 : + 36 0 : getQuery(): Query & { method: SQLiteEffectRawAction } { + 37 80 : return { ...this.dialect.sqlToQuery(this.getSQL()), method: this.action } + 38 : } + 39 : + 40 0 : mapResult(result: unknown, isFromBatch?: boolean) { + 41 64 : return isFromBatch ? this.mapBatchResult(result) : result + 42 : } + 43 : + 44 0 : _prepare(): PreparedQuery { + 45 17 : return this + 46 : } + 47 2 : } + 48 : + 49 71 : applyEffectWrapper(SQLiteEffectRaw) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 : import type * as Effect from "effect/Effect" + 3 : import type { CacheConfig } from "drizzle-orm/cache/core/types" + 4 110 : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" + 5 104 : import { entityKind, is } from "drizzle-orm/entity" + 6 : import type { + 7 : BuildSubquerySelection, + 8 : GetSelectTableName, + 9 : GetSelectTableSelection, + 10 : JoinNullability, + 11 : SelectMode, + 12 : SelectResult, + 13 : } from "drizzle-orm/query-builders/select.types" + 14 84 : import { SQL } from "drizzle-orm/sql/sql" + 15 : import type { ColumnsSelection, SQLWrapper } from "drizzle-orm/sql/sql" + 16 : import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns" + 17 : import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect" + 18 186 : import { SQLiteSelectQueryBuilderBase } from "drizzle-orm/sqlite-core/query-builders/select" + 19 : import type { + 20 : CreateSQLiteSelectFromBuilderMode, + 21 : SelectedFields, + 22 : SQLiteSelectConfig, + 23 : SQLiteSelectHKTBase, + 24 : } from "drizzle-orm/sqlite-core/query-builders/select.types" + 25 : import type { SQLiteTable } from "drizzle-orm/sqlite-core/table" + 26 134 : import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base" + 27 96 : import { Subquery } from "drizzle-orm/subquery" + 28 104 : import { type Assume, getTableColumns } from "drizzle-orm/utils" + 29 194 : import { getViewSelectedFieldsRuntime, orderSelectedFields } from "../../internal/drizzle-utils" + 30 : import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session" + 31 : + 32 : export type SQLiteEffectSelectPrepare< + 33 : T extends AnySQLiteEffectSelect, + 34 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 35 : > = SQLiteEffectPreparedQuery< + 36 : { + 37 : type: "async" + 38 : run: T["_"]["runResult"] + 39 : all: T["_"]["result"] + 40 : get: T["_"]["result"][number] | undefined + 41 : values: any[][] + 42 : execute: T["_"]["result"] + 43 : }, + 44 : TEffectHKT + 45 : > + 46 : + 47 76 : export class SQLiteEffectSelectBuilder< + 48 : TSelection extends SelectedFields | undefined, + 49 : TRunResult, + 50 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 51 : TBuilderMode extends "db" | "qb" = "db", + 52 22 : > { + 53 88 : static readonly [entityKind]: string = "SQLiteEffectSelectBuilder" + 54 : + 55 18 : private fields: TSelection + 56 20 : private session: SQLiteEffectSession<TEffectHKT, TRunResult, any> | undefined + 57 20 : private dialect: SQLiteDialect + 58 22 : private withList: Subquery[] | undefined + 59 21 : private distinct: boolean | undefined + 60 : + 61 29 : constructor(config: { + 62 : fields: TSelection + 63 : session: SQLiteEffectSession<TEffectHKT, TRunResult, any> | undefined + 64 : dialect: SQLiteDialect + 65 : withList?: Subquery[] + 66 : distinct?: boolean + 67 10 : }) { + 68 64 : this.fields = config.fields + 69 68 : this.session = config.session + 70 68 : this.dialect = config.dialect + 71 72 : this.withList = config.withList + 72 72 : this.distinct = config.distinct + 73 : } + 74 : + 75 6 : from<TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL>( + 76 16 : source: TFrom, + 77 : ): TBuilderMode extends "db" + 78 : ? SQLiteEffectSelectBase< + 79 : GetSelectTableName<TFrom>, + 80 : TRunResult, + 81 : TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection, + 82 : TSelection extends undefined ? "single" : "partial", + 83 : GetSelectTableName<TFrom> extends string ? Record<GetSelectTableName<TFrom>, "not-null"> : {}, + 84 : false, + 85 : never, + 86 : SelectResult< + 87 : TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection, + 88 : TSelection extends undefined ? "single" : "partial", + 89 : GetSelectTableName<TFrom> extends string ? Record<GetSelectTableName<TFrom>, "not-null"> : {} + 90 : >[], + 91 : BuildSubquerySelection< + 92 : TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection, + 93 : GetSelectTableName<TFrom> extends string ? Record<GetSelectTableName<TFrom>, "not-null"> : {} + 94 : >, + 95 : TEffectHKT + 96 : > + 97 : : CreateSQLiteSelectFromBuilderMode< + 98 : TBuilderMode, + 99 : GetSelectTableName<TFrom>, + 100 : "async", + 101 : TRunResult, + 102 : TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection, + 103 : TSelection extends undefined ? "single" : "partial" + 104 10 : > { + 105 84 : const isPartialSelect = !!this.fields + 106 : + 107 30 : let fields: SelectedFields + 108 47 : if (this.fields) { + 109 50 : fields = this.fields + 110 71 : } else if (is(source, Subquery)) { + 111 28 : fields = Object.fromEntries( + 112 0 : Object.keys(source._.selectedFields).map((key) => [ + 113 0 : key, + 114 17 : source[key as unknown as keyof typeof source] as unknown as SelectedFields[string], + 115 2 : ]), + 116 6 : ) + 117 83 : } else if (is(source, SQLiteViewBase)) { + 118 65 : fields = getViewSelectedFieldsRuntime(source).selectedFields as SelectedFields + 119 61 : } else if (is(source, SQL)) { + 120 16 : fields = {} + 121 27 : } else { + 122 84 : fields = getTableColumns<SQLiteTable>(source) + 123 : } + 124 : + 125 82 : return new SQLiteEffectSelectBase({ + 126 40 : table: source, + 127 26 : fields, + 128 44 : isPartialSelect, + 129 56 : session: this.session as any, + 130 56 : dialect: this.dialect, + 131 60 : withList: this.withList, + 132 54 : distinct: this.distinct, + 133 10 : }) as any + 134 : } + 135 2 : } + 136 : + 137 : export interface SQLiteEffectSelectHKT<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase> + 138 : extends SQLiteSelectHKTBase { + 139 : _type: SQLiteEffectSelectBase< + 140 : this["tableName"], + 141 : this["runResult"], + 142 : Assume<this["selection"], ColumnsSelection>, + 143 : this["selectMode"], + 144 : Assume<this["nullabilityMap"], Record<string, JoinNullability>>, + 145 : this["dynamic"], + 146 : this["excludedMethods"], + 147 : Assume<this["result"], any[]>, + 148 : Assume<this["selectedFields"], ColumnsSelection>, + 149 : TEffectHKT + 150 : > + 151 : } + 152 : + 153 : export interface SQLiteEffectSelectBase< + 154 : TTableName extends string | undefined, + 155 : TRunResult, + 156 : TSelection extends ColumnsSelection, + 157 : TSelectMode extends SelectMode = "single", + 158 : TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string + 159 : ? Record<TTableName, "not-null"> + 160 : : {}, + 161 : TDynamic extends boolean = false, + 162 : TExcludedMethods extends string = never, + 163 : TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[], + 164 : TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>, + 165 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 166 : > extends SQLiteSelectQueryBuilderBase< + 167 : SQLiteEffectSelectHKT<TEffectHKT>, + 168 : TTableName, + 169 : "async", + 170 : TRunResult, + 171 : TSelection, + 172 : TSelectMode, + 173 : TNullabilityMap, + 174 : TDynamic, + 175 : TExcludedMethods, + 176 : TResult, + 177 : TSelectedFields + 178 : >, + 179 : Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]> {} + 180 : + 181 86 : export class SQLiteEffectSelectBase< + 182 : TTableName extends string | undefined, + 183 : TRunResult, + 184 : TSelection extends ColumnsSelection, + 185 : TSelectMode extends SelectMode = "single", + 186 : TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string + 187 : ? Record<TTableName, "not-null"> + 188 : : {}, + 189 : TDynamic extends boolean = false, + 190 : TExcludedMethods extends string = never, + 191 : TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[], + 192 : TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>, + 193 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 194 : > + 195 58 : extends SQLiteSelectQueryBuilderBase< + 196 : SQLiteEffectSelectHKT<TEffectHKT>, + 197 : TTableName, + 198 : "async", + 199 : TRunResult, + 200 : TSelection, + 201 : TSelectMode, + 202 : TNullabilityMap, + 203 : TDynamic, + 204 : TExcludedMethods, + 205 : TResult, + 206 : TSelectedFields + 207 : > + 208 : implements SQLWrapper + 209 22 : { + 210 77 : static override readonly [entityKind]: string = "SQLiteEffectSelect" + 211 : + 212 28 : private get effectConfig() { + 213 43 : return (this as unknown as { config: SQLiteSelectConfig }).config + 214 : } + 215 : + 216 : /** @internal */ + 217 22 : getSQL(): SQL { + 218 117 : return this.dialect.buildSelectQuery(this.effectConfig) + 219 : } + 220 : + 221 : /** @internal */ + 222 66 : _prepare(isOneTimeQuery = true): SQLiteEffectSelectPrepare<this, TEffectHKT> { + 223 44 : if (!this.session) { + 224 106 : throw new Error("Cannot execute a query on a query builder. Please use a database instance instead.") + 225 9 : } + 226 66 : const session = this.session as unknown as SQLiteEffectSession<TEffectHKT, TRunResult, any> + 227 139 : const query = session[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"]( + 228 80 : this.dialect.sqlToQuery(this.getSQL()), + 229 94 : orderSelectedFields<SQLiteColumn>(this.effectConfig.fields), + 230 14 : "all", + 231 22 : undefined, + 232 14 : { + 233 42 : type: "select", + 234 64 : tables: [...this.usedTables], + 235 6 : }, + 236 32 : this.cacheConfig, + 237 12 : ) + 238 114 : query.joinsNotNullableMap = this.joinsNotNullableMap + 239 31 : return query as ReturnType<this["prepare"]> + 240 : } + 241 : + 242 0 : $withCache(config?: { config?: CacheConfig; tag?: string; autoInvalidate?: boolean } | false) { + 243 0 : this.cacheConfig = + 244 0 : config === undefined + 245 0 : ? { config: {}, enabled: true, autoInvalidate: true } + 246 0 : : config === false + 247 0 : ? { enabled: false } + 248 0 : : { enabled: true, autoInvalidate: true, ...config } + 249 18 : return this + 250 : } + 251 : + 252 0 : prepare(): SQLiteEffectSelectPrepare<this, TEffectHKT> { + 253 35 : return this._prepare(false) + 254 : } + 255 : + 256 0 : run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => { + 257 55 : return this._prepare().run(placeholderValues) + 258 : } + 259 : + 260 67 : all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => { + 261 100 : return this._prepare().all(placeholderValues) + 262 : } + 263 : + 264 67 : get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => { + 265 100 : return this._prepare().get(placeholderValues) + 266 : } + 267 : + 268 0 : values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => { + 269 58 : return this._prepare().values(placeholderValues) + 270 : } + 271 : + 272 0 : execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => { + 273 57 : return this._prepare().execute(placeholderValues) + 274 : } + 275 2 : } + 276 : + 277 85 : applyEffectWrapper(SQLiteEffectSelectBase) + 278 : + 279 : export type AnySQLiteEffectSelect = SQLiteEffectSelectBase<any, any, any, any, any, any, any, any, any, any> ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 : import * as Cause from "effect/Cause" + 3 : import * as Effect from "effect/Effect" + 4 : import type { SqlError } from "effect/unstable/sql/SqlError" + 5 : import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect" + 6 : import { NoopCache, strategyFor } from "drizzle-orm/cache/core/cache" + 7 : import type { WithCacheConfig } from "drizzle-orm/cache/core/types" + 8 : import { MigratorInitError } from "drizzle-orm/effect-core/errors" + 9 : import { EffectDrizzleQueryError, EffectTransactionRollbackError } from "drizzle-orm/effect-core/errors" + 10 : import type { EffectLoggerShape } from "drizzle-orm/effect-core/logger" + 11 : import type { QueryEffectHKTBase, QueryEffectKind } from "drizzle-orm/effect-core/query-effect" + 12 : import { entityKind, is } from "drizzle-orm/entity" + 13 : import type { MigrationConfig, MigrationMeta } from "drizzle-orm/migrator" + 14 : import { getMigrationsToRun } from "drizzle-orm/migrator.utils" + 15 : import type { + 16 : AnyRelations, + 17 : EmptyRelations, + 18 : RelationalQueryMapperConfig, + 19 : RelationalRowsMapper, + 20 : } from "drizzle-orm/relations" + 21 : import { makeJitRqbMapper } from "drizzle-orm/relations" + 22 : import type { PreparedQuery } from "drizzle-orm/session" + 23 : import { fillPlaceholders, type Query, type SQL, sql } from "drizzle-orm/sql/sql" + 24 : import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect" + 25 : import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types" + 26 : import type { PreparedQueryConfig, SQLiteExecuteMethod, SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session" + 27 : import { upgradeIfNeeded } from "../../up-migrations/effect-sqlite" + 28 : import { assertUnreachable, makeJitQueryMapper, type RowsMapper } from "drizzle-orm/utils" + 29 : import { mapResultRow } from "../../internal/drizzle-utils" + 30 : import { SQLiteEffectDatabase } from "./db" + 31 : + 32 : type MigrationConfigWithInit = MigrationConfig & { init?: boolean } + 33 : + 34 : type SQLiteEffectExecuteMethod = SQLiteExecuteMethod | "values" + 35 : + 36 : export class SQLiteEffectPreparedQuery< + 37 : T extends PreparedQueryConfig, + 38 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 39 : TIsRqbV2 extends boolean = false, + 40 : > implements PreparedQuery + 41 : { + 42 : static readonly [entityKind]: string = "SQLiteEffectPreparedQuery" + 43 : + 44 : /** @internal */ + 45 : joinsNotNullableMap?: Record<string, boolean> + 46 : private jitMapper?: RowsMapper<any> | RelationalRowsMapper<any> + 47 : private cacheConfig: WithCacheConfig | undefined + 48 1 : private effectExecuteMethod: SQLiteExecuteMethod + 49 : + 50 13 : constructor( + 51 78 : private executor: ( + 52 : params: unknown[], + 53 : executeMethod: SQLiteEffectExecuteMethod, + 54 : ) => Effect.Effect<unknown, unknown, unknown>, + 55 60 : protected query: Query, + 56 66 : private logger: EffectLoggerShape, + 57 60 : private cache: EffectCacheShape, + 58 108 : private queryMetadata: + 59 : | { + 60 : type: "select" | "update" | "delete" | "insert" + 61 : tables: string[] + 62 : } + 63 : | undefined, + 64 26 : cacheConfig: WithCacheConfig | undefined, + 65 66 : private fields: SelectedFieldsOrdered | undefined, + 66 30 : executeMethod: SQLiteExecuteMethod, + 67 108 : private useJitMappers: boolean | undefined, + 68 138 : private customResultMapper?: ( + 69 : rows: TIsRqbV2 extends true ? Record<string, unknown>[] : unknown[][], + 70 : mapColumnValue?: (value: unknown) => unknown, + 71 : ) => unknown, + 72 102 : private isRqbV2Query?: TIsRqbV2, + 73 84 : private rqbConfig?: RelationalQueryMapperConfig, + 74 168 : private isInTransaction: Effect.Effect<boolean> = Effect.succeed(false), + 75 10 : ) { + 76 90 : this.effectExecuteMethod = executeMethod + 77 38 : this.cacheConfig = + 78 216 : cache.strategy() === "all" && cacheConfig === undefined ? { enabled: true, autoInvalidate: true } : cacheConfig + 79 70 : if (!this.cacheConfig?.enabled) { + 80 33 : this.cacheConfig = undefined + 81 9 : } + 82 : } + 83 : + 84 : run(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["run"]> + 85 53 : run(placeholderValues?: Record<string, unknown>): any { + 86 115 : return this.executeWithCache<T["run"]>(placeholderValues, "run") + 87 : } + 88 : + 89 : all(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["all"]> + 90 53 : all(placeholderValues?: Record<string, unknown>): any { + 91 103 : if (this.isRqbV2Query) return this.allRqbV2(placeholderValues) + 92 : + 93 105 : if (!this.fields && !this.customResultMapper) { + 94 113 : return this.executeWithCache<T["all"]>(placeholderValues, "all") + 95 9 : } + 96 : + 97 58 : return this.executeWithCache<T["values"], T["all"]>( + 98 38 : placeholderValues, + 99 19 : "values", + 100 65 : (rows) => this.mapAllResult(rows) as T["all"], + 101 9 : ) + 102 : } + 103 : + 104 : get(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["get"]> + 105 53 : get(placeholderValues?: Record<string, unknown>): any { + 106 103 : if (this.isRqbV2Query) return this.getRqbV2(placeholderValues) + 107 : + 108 105 : if (!this.fields && !this.customResultMapper) { + 109 113 : return this.executeWithCache<T["get"]>(placeholderValues, "get") + 110 9 : } + 111 : + 112 58 : return this.executeWithCache<T["values"], T["get"]>( + 113 38 : placeholderValues, + 114 19 : "values", + 115 65 : (rows) => this.mapGetResult(rows) as T["get"], + 116 9 : ) + 117 : } + 118 : + 119 : values(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["values"]> + 120 0 : values(placeholderValues?: Record<string, unknown>): any { + 121 64 : return this.executeWithCache<T["values"]>(placeholderValues, "values") + 122 : } + 123 : + 124 : execute(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["execute"]> + 125 0 : execute(placeholderValues?: Record<string, unknown>): any { + 126 63 : return this[this.effectExecuteMethod](placeholderValues) as QueryEffectKind<TEffectHKT, T["execute"]> + 127 : } + 128 : + 129 0 : mapRunResult(result: unknown, _isFromBatch?: boolean): unknown { + 130 20 : return result + 131 : } + 132 : + 133 62 : mapAllResult(rows: unknown, isFromBatch?: boolean): unknown { + 134 40 : if (isFromBatch) { + 135 43 : rows = Array.isArray(rows) ? rows : [] + 136 9 : } + 137 : + 138 98 : if (!this.fields && !this.customResultMapper) { + 139 16 : return rows + 140 9 : } + 141 : + 142 52 : if (this.isRqbV2Query) { + 143 29 : return this.useJitMappers + 144 17 : ? (this.jitMapper = + 145 52 : (this.jitMapper as RelationalRowsMapper<T["all"]>) ?? makeJitRqbMapper<T["all"]>(this.rqbConfig!))( + 146 4 : rows as Record<string, unknown>[], + 147 4 : ) + 148 34 : : (this.customResultMapper as (rows: Record<string, unknown>[]) => unknown)(rows as Record<string, unknown>[]) + 149 9 : } + 150 : + 151 64 : if (this.customResultMapper) { + 152 41 : return (this.customResultMapper as (rows: unknown[][]) => unknown)(rows as unknown[][]) as T["all"] + 153 9 : } + 154 : + 155 56 : return this.useJitMappers + 156 17 : ? (this.jitMapper = + 157 18 : (this.jitMapper as RowsMapper<T["all"]>) ?? + 158 69 : makeJitQueryMapper<T["all"]>(this.fields!, this.joinsNotNullableMap))(rows as unknown[][]) + 159 155 : : (rows as unknown[][]).map((row) => mapResultRow(this.fields!, row, this.joinsNotNullableMap)) + 160 : } + 161 : + 162 62 : mapGetResult(rows: unknown, isFromBatch?: boolean): unknown { + 163 40 : if (isFromBatch) { + 164 43 : rows = Array.isArray(rows) ? rows : [] + 165 9 : } + 166 : + 167 98 : if (!this.fields && !this.customResultMapper) { + 168 48 : return Array.isArray(rows) ? rows[0] : rows + 169 9 : } + 170 : + 171 97 : const row = Array.isArray(rows) ? rows[0] : rows + 172 50 : if (!row) return undefined + 173 : + 174 52 : if (this.isRqbV2Query) { + 175 29 : return this.useJitMappers + 176 17 : ? (this.jitMapper = + 177 61 : (this.jitMapper as RelationalRowsMapper<T["get"][]>) ?? makeJitRqbMapper<T["get"][]>(this.rqbConfig!))([ + 178 9 : row as Record<string, unknown>, + 179 5 : ]) + 180 35 : : (this.customResultMapper as (rows: Record<string, unknown>[]) => unknown)([row as Record<string, unknown>]) + 181 9 : } + 182 : + 183 64 : if (this.customResultMapper) { + 184 42 : return (this.customResultMapper as (rows: unknown[][]) => unknown)([row as unknown[]]) as T["get"] + 185 9 : } + 186 : + 187 56 : return this.useJitMappers + 188 17 : ? (this.jitMapper = + 189 18 : (this.jitMapper as RowsMapper<T["get"][]>) ?? + 190 73 : makeJitQueryMapper<T["get"][]>(this.fields!, this.joinsNotNullableMap))([row as unknown[]])[0] + 191 119 : : mapResultRow(this.fields!, row as unknown[], this.joinsNotNullableMap) + 192 : } + 193 : + 194 0 : private allRqbV2(placeholderValues?: Record<string, unknown>) { + 195 0 : return this.executeWithCache<unknown[], T["all"]>( + 196 0 : placeholderValues, + 197 0 : "all", + 198 0 : (rows) => this.mapAllResult(rows) as T["all"], + 199 8 : ) + 200 : } + 201 : + 202 0 : private getRqbV2(placeholderValues?: Record<string, unknown>) { + 203 0 : return this.executeWithCache<unknown, T["get"] | undefined>(placeholderValues, "get", (row) => + 204 0 : row === undefined ? undefined : (this.mapGetResult(row) as T["get"]), + 205 8 : ) + 206 : } + 207 : + 208 18 : private executeWithCache<A, B = A>( + 209 38 : placeholderValues: Record<string, unknown> | undefined, + 210 30 : executeMethod: SQLiteEffectExecuteMethod, + 211 22 : mapResult?: (result: A) => B, + 212 10 : ) { + 213 96 : return Effect.gen({ self: this }, function* () { + 214 164 : const params = fillPlaceholders(this.query.params, placeholderValues ?? {}) + 215 : + 216 116 : yield* this.logger.logQuery(this.query.sql, params) + 217 : + 218 68 : return yield* this.queryWithCache( + 219 32 : this.query.sql, + 220 16 : params, + 221 118 : Effect.suspend(() => this.executor(params, executeMethod) as Effect.Effect<A, unknown, unknown>), + 222 18 : mapResult, + 223 9 : ) + 224 9 : }) + 225 : } + 226 : + 227 65 : private mapCachedResult<A, B>(result: A, mapResult: ((result: A) => B) | undefined) { + 228 108 : if (!mapResult) return Effect.succeed(result as unknown as B) + 229 50 : return Effect.try({ + 230 68 : try: () => mapResult(result), + 231 37 : catch: (cause) => cause, + 232 11 : }) + 233 : } + 234 : + 235 16 : private queryWithCache<A, E, R, B = A>( + 236 26 : queryString: string, + 237 16 : params: unknown[], + 238 14 : query: Effect.Effect<A, E, R>, + 239 22 : mapResult?: (result: A) => B, + 240 10 : ) { + 241 96 : return Effect.gen({ self: this }, function* () { + 242 227 : if (this.queryMetadata?.type === "select" && this.cacheConfig?.enabled && (yield* this.isInTransaction)) { + 243 125 : return yield* this.mapCachedResult(yield* query, mapResult) + 244 13 : } + 245 : + 246 107 : const cacheStrat: Awaited<ReturnType<typeof strategyFor>> = !is(this.cache.cache, NoopCache) + 247 107 : ? yield* Effect.tryPromise(() => strategyFor(queryString, params, this.queryMetadata, this.cacheConfig)) + 248 45 : : { type: "skip" as const } + 249 : + 250 81 : if (cacheStrat.type === "skip") { + 251 125 : return yield* this.mapCachedResult(yield* query, mapResult) + 252 7 : } + 253 : + 254 47 : if (cacheStrat.type === "invalidate") { + 255 36 : const result = yield* query + 256 66 : yield* this.cache.onMutate({ tables: cacheStrat.tables }) + 257 60 : return yield* this.mapCachedResult(result, mapResult) + 258 7 : } + 259 : + 260 40 : if (cacheStrat.type === "try") { + 261 44 : if (yield* this.isInTransaction) { + 262 68 : return yield* this.mapCachedResult(yield* query, mapResult) + 263 9 : } + 264 : + 265 74 : const { tables, key, isTag, autoInvalidate, config } = cacheStrat + 266 84 : const fromCache: any[] | undefined = yield* this.cache.get(key, tables, isTag, autoInvalidate) + 267 : + 268 49 : if (typeof fromCache !== "undefined") { + 269 65 : return yield* this.mapCachedResult(fromCache as unknown as A, mapResult) + 270 9 : } + 271 : + 272 36 : const result = yield* query + 273 : + 274 88 : yield* this.cache.put(key, result, autoInvalidate ? tables : [], isTag, config) + 275 : + 276 60 : return yield* this.mapCachedResult(result, mapResult) + 277 7 : } + 278 : + 279 36 : assertUnreachable(cacheStrat) + 280 14 : }).pipe( + 281 53 : Effect.catch((e) => { + 282 208 : return Effect.fail(new EffectDrizzleQueryError({ query: queryString, params, cause: Cause.fail(e) })) + 283 2 : }), + 284 9 : ) + 285 : } + 286 : + 287 0 : getQuery(): Query { + 288 24 : return this.query + 289 : } + 290 : + 291 0 : mapResult(response: unknown, isFromBatch?: boolean) { + 292 0 : switch (this.effectExecuteMethod) { + 293 0 : case "run": { + 294 0 : return this.mapRunResult(response, isFromBatch) + 295 0 : } + 296 0 : case "all": { + 297 0 : return this.mapAllResult(response, isFromBatch) + 298 0 : } + 299 0 : case "get": { + 300 0 : return this.mapGetResult(response, isFromBatch) + 301 11 : } + 302 : } + 303 : } + 304 2 : } + 305 : + 306 64 : export abstract class SQLiteEffectSession< + 307 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 308 : TRunResult = unknown, + 309 : TRelations extends AnyRelations = EmptyRelations, + 310 6 : > { + 311 75 : static readonly [entityKind]: string = "SQLiteEffectSession" + 312 : + 313 131 : constructor(readonly dialect: SQLiteAsyncDialect) {} + 314 : + 315 : abstract prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>( + 316 : query: Query, + 317 : fields: SelectedFieldsOrdered | undefined, + 318 : executeMethod: SQLiteExecuteMethod, + 319 : customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown, + 320 : queryMetadata?: { + 321 : type: "select" | "update" | "delete" | "insert" + 322 : tables: string[] + 323 : }, + 324 : cacheConfig?: WithCacheConfig, + 325 : ): SQLiteEffectPreparedQuery<T, TEffectHKT> + 326 : + 327 21 : prepareOneTimeQuery<T extends PreparedQueryConfig = PreparedQueryConfig>( + 328 14 : query: Query, + 329 16 : fields: SelectedFieldsOrdered | undefined, + 330 30 : executeMethod: SQLiteExecuteMethod, + 331 40 : customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown, + 332 30 : queryMetadata?: { + 333 : type: "select" | "update" | "delete" | "insert" + 334 : tables: string[] + 335 : }, + 336 26 : cacheConfig?: WithCacheConfig, + 337 10 : ): SQLiteEffectPreparedQuery<T, TEffectHKT> { + 338 211 : return this.prepareQuery(query, fields, executeMethod, customResultMapper, queryMetadata, cacheConfig) + 339 : } + 340 : + 341 : abstract prepareRelationalQuery<T extends PreparedQueryConfig = PreparedQueryConfig>( + 342 : query: Query, + 343 : fields: SelectedFieldsOrdered | undefined, + 344 : executeMethod: SQLiteExecuteMethod, + 345 : customResultMapper: (rows: Record<string, unknown>[], mapColumnValue?: (value: unknown) => unknown) => unknown, + 346 : config: RelationalQueryMapperConfig, + 347 : ): SQLiteEffectPreparedQuery<T, TEffectHKT, true> + 348 : + 349 0 : prepareOneTimeRelationalQuery<T extends PreparedQueryConfig = PreparedQueryConfig>( + 350 0 : query: Query, + 351 0 : fields: SelectedFieldsOrdered | undefined, + 352 0 : executeMethod: SQLiteExecuteMethod, + 353 0 : customResultMapper: (rows: Record<string, unknown>[], mapColumnValue?: (value: unknown) => unknown) => unknown, + 354 0 : config: RelationalQueryMapperConfig, + 355 0 : ): SQLiteEffectPreparedQuery<T, TEffectHKT, true> { + 356 99 : return this.prepareRelationalQuery(query, fields, executeMethod, customResultMapper, config) + 357 : } + 358 : + 359 : run(query: SQL): QueryEffectKind<TEffectHKT, TRunResult> + 360 29 : run(query: SQL): any { + 361 50 : return this.prepareQuery<PreparedQueryConfig & { run: TRunResult; execute: TRunResult }>( + 362 64 : this.dialect.sqlToQuery(query), + 363 22 : undefined, + 364 10 : "run", + 365 21 : ).run() + 366 : } + 367 : + 368 : all<T = unknown>(query: SQL): QueryEffectKind<TEffectHKT, T[]> + 369 29 : all<T = unknown>(query: SQL): any { + 370 50 : return this.prepareQuery<PreparedQueryConfig & { all: T[]; execute: T[] }>( + 371 64 : this.dialect.sqlToQuery(query), + 372 22 : undefined, + 373 10 : "all", + 374 21 : ).all() + 375 : } + 376 : + 377 : get<T = unknown>(query: SQL): QueryEffectKind<TEffectHKT, T | undefined> + 378 29 : get<T = unknown>(query: SQL): any { + 379 50 : return this.prepareQuery<PreparedQueryConfig & { get: T | undefined; execute: T | undefined }>( + 380 64 : this.dialect.sqlToQuery(query), + 381 22 : undefined, + 382 10 : "get", + 383 21 : ).get() + 384 : } + 385 : + 386 : values<T extends unknown[] = unknown[]>(query: SQL): QueryEffectKind<TEffectHKT, T[]> + 387 0 : values<T extends unknown[] = unknown[]>(query: SQL): any { + 388 0 : return this.prepareQuery<PreparedQueryConfig & { values: T[]; execute: T[] }>( + 389 0 : this.dialect.sqlToQuery(query), + 390 0 : undefined, + 391 0 : "all", + 392 17 : ).values() + 393 : } + 394 : + 395 : count(query: SQL): QueryEffectKind<TEffectHKT, number> + 396 0 : count(query: SQL): any { + 397 81 : return this.values<[number]>(query).pipe(Effect.map((result) => result[0]?.[0] ?? 0)) + 398 : } + 399 : + 400 : abstract transaction<A, E, R>( + 401 : transaction: (tx: SQLiteEffectTransaction<TEffectHKT, TRunResult, TRelations>) => Effect.Effect<A, E, R>, + 402 : config?: SQLiteTransactionConfig, + 403 : ): Effect.Effect<A, E | SqlError, R> + 404 2 : } + 405 : + 406 88 : export abstract class SQLiteEffectTransaction< + 407 : TEffectHKT extends QueryEffectHKTBase, + 408 : TRunResult, + 409 : TRelations extends AnyRelations = EmptyRelations, + 410 48 : > extends SQLiteEffectDatabase<TEffectHKT, TRunResult, TRelations> { + 411 83 : static override readonly [entityKind]: string = "SQLiteEffectTransaction" + 412 : + 413 13 : constructor( + 414 18 : dialect: SQLiteAsyncDialect, + 415 18 : session: SQLiteEffectSession<TEffectHKT, TRunResult, TRelations>, + 416 124 : protected relations: TRelations, + 417 10 : ) { + 418 78 : super(dialect, session, relations) + 419 : } + 420 : + 421 0 : rollback() { + 422 47 : return new EffectTransactionRollbackError() + 423 : } + 424 2 : } + 425 : + 426 0 : export const migrate = Effect.fn("migrate")(function* <TEffectHKT extends QueryEffectHKTBase>( + 427 0 : migrations: MigrationMeta[], + 428 0 : session: SQLiteEffectSession<TEffectHKT>, + 429 0 : config: string | MigrationConfigWithInit, + 430 0 : ) { + 431 0 : const migrationsTable = + 432 0 : typeof config === "string" ? "__drizzle_migrations" : (config.migrationsTable ?? "__drizzle_migrations") + 433 0 : + 434 0 : const { newDb } = yield* upgradeIfNeeded(migrationsTable, session, migrations) + 435 0 : + 436 0 : if (newDb) { + 437 0 : yield* session.run(sql` + 438 0 : CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsTable)} ( + 439 0 : id INTEGER PRIMARY KEY, + 440 0 : hash text NOT NULL, + 441 0 : created_at numeric, + 442 0 : name text, + 443 0 : applied_at TEXT + 444 0 : ) + 445 0 : `) + 446 0 : } + 447 0 : + 448 0 : const dbMigrations = yield* session.all<{ id: number; hash: string; created_at: string; name: string | null }>( + 449 0 : sql`SELECT id, hash, created_at, name FROM ${sql.identifier(migrationsTable)}`, + 450 0 : ) + 451 0 : + 452 0 : if (typeof config === "object" && config.init) { + 453 0 : if (dbMigrations.length) { + 454 0 : return yield* new MigratorInitError({ exitCode: "databaseMigrations" }) + 455 0 : } + 456 0 : + 457 0 : if (migrations.length > 1) { + 458 0 : return yield* new MigratorInitError({ exitCode: "localMigrations" }) + 459 0 : } + 460 0 : + 461 0 : const [migration] = migrations + 462 0 : if (!migration) return + 463 0 : + 464 0 : yield* session.run( + 465 0 : sql`insert into ${sql.identifier( + 466 0 : migrationsTable, + 467 0 : )} ("hash", "created_at", "name", "applied_at") values(${migration.hash}, ${migration.folderMillis}, ${migration.name}, ${new Date().toISOString()})`, + 468 0 : ) + 469 0 : + 470 0 : return + 471 0 : } + 472 0 : + 473 0 : const migrationsToRun = getMigrationsToRun({ localMigrations: migrations, dbMigrations }) + 474 0 : if (migrationsToRun.length === 0) return + 475 0 : + 476 0 : yield* session.transaction((tx) => + 477 0 : Effect.gen(function* () { + 478 0 : for (const migration of migrationsToRun) { + 479 0 : for (const stmt of migration.sql) { + 480 0 : yield* tx.run(sql.raw(stmt)) + 481 0 : } + 482 0 : yield* tx.run( + 483 0 : sql`insert into ${sql.identifier( + 484 0 : migrationsTable, + 485 0 : )} ("hash", "created_at", "name", "applied_at") values(${migration.hash}, ${migration.folderMillis}, ${migration.name}, ${new Date().toISOString()})`, + 486 0 : ) + 487 0 : } + 488 0 : }), + 489 3 : ) + 490 5 : }) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 : import type * as Effect from "effect/Effect" + 3 : import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" + 4 : import { entityKind, is } from "drizzle-orm/entity" + 5 : import type { SelectResultFields } from "drizzle-orm/query-builders/select.types" + 6 : import type { RunnableQuery } from "drizzle-orm/runnable-query" + 7 : import { SelectionProxyHandler } from "drizzle-orm/selection-proxy" + 8 : import type { Placeholder, Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql" + 9 : import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect" + 10 : import type { SelectedFields, SQLiteSelectJoinConfig } from "drizzle-orm/sqlite-core/query-builders/select.types" + 11 : import type { SQLiteUpdateConfig, SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update" + 12 : import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session" + 13 : import { SQLiteTable } from "drizzle-orm/sqlite-core/table" + 14 : import { extractUsedTable } from "drizzle-orm/sqlite-core/utils" + 15 : import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base" + 16 : import { Subquery } from "drizzle-orm/subquery" + 17 : import { type DrizzleTypeError, type UpdateSet, type ValueOrArray } from "drizzle-orm/utils" + 18 : import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common" + 19 : import { + 20 : getTableColumnsRuntime, + 21 : getTableLikeName, + 22 : getViewSelectedFieldsRuntime, + 23 : mapUpdateSet, + 24 : orderSelectedFields, + 25 : } from "../../internal/drizzle-utils" + 26 : import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session" + 27 : + 28 : export type SQLiteEffectUpdateWithout< + 29 : T extends AnySQLiteEffectUpdate, + 30 : TDynamic extends boolean, + 31 : K extends keyof T & string, + 32 : > = TDynamic extends true + 33 : ? T + 34 : : Omit< + 35 : SQLiteEffectUpdateBase< + 36 : T["_"]["table"], + 37 : T["_"]["runResult"], + 38 : T["_"]["from"], + 39 : T["_"]["returning"], + 40 : TDynamic, + 41 : T["_"]["excludedMethods"] | K, + 42 : T["_"]["effectHKT"] + 43 : >, + 44 : T["_"]["excludedMethods"] | K + 45 : > + 46 : + 47 : export type SQLiteEffectUpdateWithJoins< + 48 : T extends AnySQLiteEffectUpdate, + 49 : TDynamic extends boolean, + 50 : TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL, + 51 : > = TDynamic extends true + 52 : ? T + 53 : : Omit< + 54 : SQLiteEffectUpdateBase< + 55 : T["_"]["table"], + 56 : T["_"]["runResult"], + 57 : TFrom, + 58 : T["_"]["returning"], + 59 : TDynamic, + 60 : Exclude<T["_"]["excludedMethods"] | "from", "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin">, + 61 : T["_"]["effectHKT"] + 62 : >, + 63 : Exclude<T["_"]["excludedMethods"] | "from", "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin"> + 64 : > + 65 : + 66 : export type SQLiteEffectUpdateReturningAll< + 67 : T extends AnySQLiteEffectUpdate, + 68 : TDynamic extends boolean, + 69 : > = SQLiteEffectUpdateWithout< + 70 : SQLiteEffectUpdateBase< + 71 : T["_"]["table"], + 72 : T["_"]["runResult"], + 73 : T["_"]["from"], + 74 : T["_"]["table"]["$inferSelect"], + 75 : TDynamic, + 76 : T["_"]["excludedMethods"], + 77 : T["_"]["effectHKT"] + 78 : >, + 79 : TDynamic, + 80 : "returning" + 81 : > + 82 : + 83 : export type SQLiteEffectUpdateReturning< + 84 : T extends AnySQLiteEffectUpdate, + 85 : TDynamic extends boolean, + 86 : TSelectedFields extends SelectedFields, + 87 : > = SQLiteEffectUpdateWithout< + 88 : SQLiteEffectUpdateBase< + 89 : T["_"]["table"], + 90 : T["_"]["runResult"], + 91 : T["_"]["from"], + 92 : SelectResultFields<TSelectedFields>, + 93 : TDynamic, + 94 : T["_"]["excludedMethods"], + 95 : T["_"]["effectHKT"] + 96 : >, + 97 : TDynamic, + 98 : "returning" + 99 : > + 100 : + 101 : export type SQLiteEffectUpdateExecute<T extends AnySQLiteEffectUpdate> = T["_"]["returning"] extends undefined + 102 : ? T["_"]["runResult"] + 103 : : T["_"]["returning"][] + 104 : + 105 : export type SQLiteEffectUpdatePrepare< + 106 : T extends AnySQLiteEffectUpdate, + 107 : TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"], + 108 : > = SQLiteEffectPreparedQuery< + 109 : PreparedQueryConfig & { + 110 : run: T["_"]["runResult"] + 111 : all: T["_"]["returning"] extends undefined + 112 : ? DrizzleTypeError<".all() cannot be used without .returning()"> + 113 : : T["_"]["returning"][] + 114 : get: T["_"]["returning"] extends undefined + 115 : ? DrizzleTypeError<".get() cannot be used without .returning()"> + 116 : : T["_"]["returning"] + 117 : values: T["_"]["returning"] extends undefined + 118 : ? DrizzleTypeError<".values() cannot be used without .returning()"> + 119 : : any[][] + 120 : execute: SQLiteEffectUpdateExecute<T> + 121 : }, + 122 : TEffectHKT + 123 : > + 124 : + 125 : export type SQLiteEffectUpdateDynamic<T extends AnySQLiteEffectUpdate> = SQLiteEffectUpdate< + 126 : T["_"]["table"], + 127 : T["_"]["runResult"], + 128 : T["_"]["from"], + 129 : T["_"]["returning"], + 130 : T["_"]["effectHKT"] + 131 : > + 132 : + 133 : export type SQLiteEffectUpdate< + 134 : TTable extends SQLiteTable = SQLiteTable, + 135 : TRunResult = unknown, + 136 : TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined, + 137 : TReturning extends Record<string, unknown> | undefined = Record<string, unknown> | undefined, + 138 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 139 : > = SQLiteEffectUpdateBase<TTable, TRunResult, TFrom, TReturning, true, never, TEffectHKT> + 140 : + 141 : export type AnySQLiteEffectUpdate = SQLiteEffectUpdateBase<any, any, any, any, any, any, any> + 142 : + 143 : export type SQLiteEffectUpdateJoinFn<T extends AnySQLiteEffectUpdate> = < + 144 : TJoinedTable extends SQLiteTable | Subquery | SQLiteViewBase | SQL, + 145 : >( + 146 : table: TJoinedTable, + 147 : on: + 148 : | (( + 149 : updateTable: T["_"]["table"]["_"]["columns"], + 150 : from: T["_"]["from"] extends SQLiteTable + 151 : ? T["_"]["from"]["_"]["columns"] + 152 : : T["_"]["from"] extends Subquery | SQLiteViewBase + 153 : ? T["_"]["from"]["_"]["selectedFields"] + 154 : : never, + 155 : ) => SQL | undefined) + 156 : | SQL + 157 : | undefined, + 158 : ) => T + 159 : + 160 : export class SQLiteEffectUpdateBuilder< + 161 : TTable extends SQLiteTable, + 162 : TRunResult, + 163 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 164 : > { + 165 1 : static readonly [entityKind]: string = "SQLiteEffectUpdateBuilder" + 166 : + 167 : declare readonly _: { + 168 : readonly table: TTable + 169 : } + 170 : + 171 13 : constructor( + 172 60 : protected table: TTable, + 173 72 : protected session: SQLiteEffectSession<TEffectHKT, TRunResult, any>, + 174 72 : protected dialect: SQLiteDialect, + 175 78 : private withList?: Subquery[], + 176 10 : ) {} + 177 : + 178 5 : set( + 179 16 : values: SQLiteUpdateSetSource<TTable>, + 180 : ): SQLiteEffectUpdateWithout< + 181 : SQLiteEffectUpdateBase<TTable, TRunResult, undefined, undefined, false, never, TEffectHKT>, + 182 : false, + 183 : "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin" + 184 10 : > { + 185 68 : return new SQLiteEffectUpdateBase( + 186 24 : this.table, + 187 68 : mapUpdateSet(this.table, values), + 188 28 : this.session, + 189 28 : this.dialect, + 190 26 : this.withList, + 191 8 : ) as any + 192 : } + 193 2 : } + 194 : + 195 : export interface SQLiteEffectUpdateBase< + 196 : TTable extends SQLiteTable = SQLiteTable, + 197 : TRunResult = unknown, + 198 : TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined, + 199 : TReturning = undefined, + 200 : TDynamic extends boolean = false, + 201 : _TExcludedMethods extends string = never, + 202 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 203 : > extends SQLWrapper, + 204 : RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, + 205 : Effect.Effect< + 206 : TReturning extends undefined ? TRunResult : TReturning[], + 207 : TEffectHKT["error"], + 208 : TEffectHKT["context"] + 209 : > { + 210 : readonly _: { + 211 : readonly dialect: "sqlite" + 212 : readonly table: TTable + 213 : readonly resultType: "async" + 214 : readonly runResult: TRunResult + 215 : readonly from: TFrom + 216 : readonly returning: TReturning + 217 : readonly dynamic: TDynamic + 218 : readonly excludedMethods: _TExcludedMethods + 219 : readonly result: TReturning extends undefined ? TRunResult : TReturning[] + 220 : readonly effectHKT: TEffectHKT + 221 : } + 222 : } + 223 : + 224 70 : export class SQLiteEffectUpdateBase< + 225 : TTable extends SQLiteTable = SQLiteTable, + 226 : TRunResult = unknown, + 227 : TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined, + 228 : TReturning = undefined, + 229 : TDynamic extends boolean = false, + 230 : _TExcludedMethods extends string = never, + 231 : TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + 232 : > + 233 : implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper + 234 6 : { + 235 74 : static readonly [entityKind]: string = "SQLiteEffectUpdate" + 236 : + 237 : /** @internal */ + 238 17 : config: SQLiteUpdateConfig + 239 : + 240 13 : constructor( + 241 14 : table: TTable, + 242 10 : set: UpdateSet, + 243 140 : private effectSession: SQLiteEffectSession<TEffectHKT, TRunResult, any>, + 244 156 : private effectDialect: SQLiteDialect, + 245 20 : withList?: Subquery[], + 246 10 : ) { + 247 108 : this.config = { set, table, withList, joins: [] } + 248 : } + 249 : + 250 0 : from<TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL>( + 251 0 : source: TFrom, + 252 0 : ): SQLiteEffectUpdateWithJoins<this, TDynamic, TFrom> { + 253 0 : this.config.from = source + 254 18 : return this as any + 255 : } + 256 : + 257 12 : private createJoin<TJoinType extends SQLiteSelectJoinConfig["joinType"]>( + 258 20 : joinType: TJoinType, + 259 10 : ): SQLiteEffectUpdateJoinFn<this> { + 260 0 : return (( + 261 0 : table: SQLiteTable | Subquery | SQLiteViewBase | SQL, + 262 0 : on: ((updateTable: TTable, from: TFrom) => SQL | undefined) | SQL | undefined, + 263 0 : ) => { + 264 0 : const tableName = getTableLikeName(table) + 265 0 : + 266 0 : if (typeof tableName === "string" && this.config.joins.some((join) => join.alias === tableName)) { + 267 0 : throw new Error(`Alias "${tableName}" is already used in this query`) + 268 0 : } + 269 0 : + 270 0 : if (typeof on === "function") { + 271 0 : const from = this.config.from + 272 0 : ? is(table, SQLiteTable) + 273 0 : ? getTableColumnsRuntime(table) + 274 0 : : is(table, Subquery) + 275 0 : ? table._.selectedFields + 276 0 : : is(table, SQLiteViewBase) + 277 0 : ? getViewSelectedFieldsRuntime(table).selectedFields + 278 0 : : undefined + 279 0 : : undefined + 280 0 : on = on( + 281 0 : new Proxy( + 282 0 : this.config.table._.columns, + 283 0 : new SelectionProxyHandler({ sqlAliasedBehavior: "sql", sqlBehavior: "sql" }), + 284 0 : ) as any, + 285 0 : from && + 286 0 : (new Proxy(from, new SelectionProxyHandler({ sqlAliasedBehavior: "sql", sqlBehavior: "sql" })) as any), + 287 0 : ) + 288 0 : } + 289 0 : + 290 0 : this.config.joins.push({ on, table, joinType, alias: tableName }) + 291 0 : + 292 25 : return this as any + 293 : }) as any + 294 : } + 295 : + 296 74 : leftJoin = this.createJoin("left") + 297 : + 298 78 : rightJoin = this.createJoin("right") + 299 : + 300 78 : innerJoin = this.createJoin("inner") + 301 : + 302 73 : fullJoin = this.createJoin("full") + 303 : + 304 31 : where(where: SQL | undefined): SQLiteEffectUpdateWithout<this, TDynamic, "where"> { + 305 60 : this.config.where = where + 306 29 : return this as any + 307 : } + 308 : + 309 : orderBy( + 310 : builder: (updateTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>, + 311 : ): SQLiteEffectUpdateWithout<this, TDynamic, "orderBy"> + 312 : orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): SQLiteEffectUpdateWithout<this, TDynamic, "orderBy"> + 313 0 : orderBy( + 314 0 : ...columns: + 315 0 : | [(updateTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>] + 316 0 : | (SQLiteColumn | SQL | SQL.Aliased)[] + 317 0 : ): SQLiteEffectUpdateWithout<this, TDynamic, "orderBy"> { + 318 0 : if (typeof columns[0] === "function") { + 319 0 : const orderBy = columns[0]( + 320 0 : new Proxy( + 321 0 : getTableColumnsRuntime(this.config.table), + 322 0 : new SelectionProxyHandler({ sqlAliasedBehavior: "alias", sqlBehavior: "sql" }), + 323 0 : ) as any, + 324 0 : ) + 325 0 : + 326 0 : this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy] + 327 0 : return this as any + 328 0 : } + 329 0 : + 330 0 : this.config.orderBy = columns as (SQLiteColumn | SQL | SQL.Aliased)[] + 331 18 : return this as any + 332 : } + 333 : + 334 0 : limit(limit: number | Placeholder): SQLiteEffectUpdateWithout<this, TDynamic, "limit"> { + 335 0 : this.config.limit = limit + 336 18 : return this as any + 337 : } + 338 : + 339 : returning(): SQLiteEffectUpdateReturningAll<this, TDynamic> + 340 : returning<TSelectedFields extends SelectedFields>( + 341 : fields: TSelectedFields, + 342 : ): SQLiteEffectUpdateReturning<this, TDynamic, TSelectedFields> + 343 11 : returning( + 344 104 : fields: SelectedFields = getTableColumnsRuntime(this.config.table), + 345 10 : ): SQLiteEffectUpdateWithout<AnySQLiteEffectUpdate, TDynamic, "returning"> { + 346 112 : this.config.returning = orderSelectedFields<SQLiteColumn>(fields) + 347 29 : return this as any + 348 : } + 349 : + 350 : /** @internal */ + 351 22 : getSQL(): SQL { + 352 117 : return this.effectDialect.buildUpdateQuery(this.config) + 353 : } + 354 : + 355 0 : toSQL(): Query { + 356 58 : return this.effectDialect.sqlToQuery(this.getSQL()) + 357 : } + 358 : + 359 : /** @internal */ + 360 66 : _prepare(isOneTimeQuery = true): SQLiteEffectUpdatePrepare<this, TEffectHKT> { + 361 147 : return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"]( + 362 92 : this.effectDialect.sqlToQuery(this.getSQL()), + 363 46 : this.config.returning, + 364 75 : this.config.returning ? "all" : "run", + 365 22 : undefined, + 366 14 : { + 367 42 : type: "update", + 368 94 : tables: extractUsedTable(this.config.table), + 369 2 : }, + 370 9 : ) as SQLiteEffectUpdatePrepare<this, TEffectHKT> + 371 : } + 372 : + 373 0 : prepare(): SQLiteEffectUpdatePrepare<this, TEffectHKT> { + 374 35 : return this._prepare(false) + 375 : } + 376 : + 377 67 : run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => { + 378 100 : return this._prepare().run(placeholderValues) + 379 : } + 380 : + 381 0 : all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => { + 382 55 : return this._prepare().all(placeholderValues) + 383 : } + 384 : + 385 67 : get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => { + 386 100 : return this._prepare().get(placeholderValues) + 387 : } + 388 : + 389 0 : values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => { + 390 58 : return this._prepare().values(placeholderValues) + 391 : } + 392 : + 393 0 : execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => { + 394 58 : return this._prepare().execute(placeholderValues) + 395 : } + 396 : + 397 0 : $dynamic(): SQLiteEffectUpdateDynamic<this> { + 398 17 : return this as any + 399 : } + 400 2 : } + 401 : + 402 85 : applyEffectWrapper(SQLiteEffectUpdateBase) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 40 : import * as Effect from "effect/Effect" + 3 : import type { SqlError } from "effect/unstable/sql/SqlError" + 4 68 : import { EffectDrizzleError } from "drizzle-orm/effect-core/errors" + 5 : import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" + 6 : import type { MigrationMeta } from "drizzle-orm/migrator" + 7 42 : import { sql } from "drizzle-orm/sql/sql" + 8 : import type { SQLiteEffectSession } from "../sqlite-core/effect/session" + 9 99 : import { + 10 : buildSQLiteMigrationBackfillStatements, + 11 : prepareSQLiteMigrationBackfill, + 12 : type SQLiteMigrationTableRow, + 13 : } from "./sqlite" + 14 69 : import { GET_VERSION_FOR, MIGRATIONS_TABLE_VERSIONS, type UpgradeResult } from "./utils" + 15 : + 16 0 : const migrationUpgradeError = (cause: unknown) => + 17 0 : new EffectDrizzleError({ + 18 0 : message: + 19 0 : typeof cause === "object" && cause !== null && "message" in cause && typeof cause.message === "string" + 20 0 : ? cause.message + 21 0 : : String(cause), + 22 : cause, + 23 2 : }) + 24 : + 25 30 : export const upgradeIfNeeded: <TEffectHKT extends QueryEffectHKTBase>( + 26 : migrationsTable: string, + 27 : session: SQLiteEffectSession<TEffectHKT>, + 28 : localMigrations: MigrationMeta[], + 29 : ) => Effect.Effect<UpgradeResult, EffectDrizzleError | TEffectHKT["error"] | SqlError, TEffectHKT["context"]> = + 30 0 : Effect.fn("upgradeIfNeeded")(function* <TEffectHKT extends QueryEffectHKTBase>( + 31 0 : migrationsTable: string, + 32 0 : session: SQLiteEffectSession<TEffectHKT>, + 33 0 : localMigrations: MigrationMeta[], + 34 0 : ) { + 35 0 : const tableExists = yield* session.all( + 36 0 : sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`, + 37 0 : ) + 38 0 : + 39 0 : if (tableExists.length === 0) { + 40 0 : return { newDb: true } + 41 0 : } + 42 0 : + 43 0 : const rows = yield* session.all<{ column_name: string }>( + 44 0 : sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`, + 45 0 : ) + 46 0 : + 47 0 : const version = GET_VERSION_FOR.sqlite(rows.map((r) => r.column_name)) + 48 0 : + 49 0 : for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++) { + 50 0 : const upgradeFn = upgradeFunctions[v] + 51 0 : if (!upgradeFn) { + 52 0 : return yield* new EffectDrizzleError({ + 53 0 : message: `No upgrade path from migration table version ${v} to ${v + 1}`, + 54 0 : cause: { version: v }, + 55 0 : }) + 56 0 : } + 57 0 : yield* upgradeFn(migrationsTable, session, localMigrations) + 58 0 : } + 59 0 : + 60 : return { newDb: false } + 61 3 : }) + 62 : + 63 24 : const upgradeFunctions: Record< + 64 : number, + 65 : <TEffectHKT extends QueryEffectHKTBase>( + 66 : migrationsTable: string, + 67 : session: SQLiteEffectSession<TEffectHKT>, + 68 : localMigrations: MigrationMeta[], + 69 : ) => Effect.Effect<void, EffectDrizzleError | TEffectHKT["error"] | SqlError, TEffectHKT["context"]> + 70 3 : > = { + 71 17 : 0: upgradeFromV0, + 72 1 : } + 73 : + 74 0 : function upgradeFromV0<TEffectHKT extends QueryEffectHKTBase>( + 75 0 : migrationsTable: string, + 76 0 : session: SQLiteEffectSession<TEffectHKT>, + 77 0 : localMigrations: MigrationMeta[], + 78 0 : ): Effect.Effect<void, EffectDrizzleError | TEffectHKT["error"] | SqlError, TEffectHKT["context"]> { + 79 0 : return Effect.gen(function* () { + 80 0 : const table = sql`${sql.identifier(migrationsTable)}` + 81 0 : + 82 0 : const dbRows = yield* session.all<SQLiteMigrationTableRow>( + 83 0 : sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`, + 84 0 : ) + 85 0 : const statements = yield* Effect.try({ + 86 0 : try: () => + 87 0 : buildSQLiteMigrationBackfillStatements( + 88 0 : migrationsTable, + 89 0 : prepareSQLiteMigrationBackfill(dbRows, localMigrations), + 90 0 : ), + 91 0 : catch: migrationUpgradeError, + 92 0 : }) + 93 0 : + 94 0 : yield* session.transaction((tx) => + 95 0 : Effect.gen(function* () { + 96 0 : for (const statement of statements) { + 97 0 : yield* tx.run(statement) + 98 0 : } + 99 0 : }), + 100 0 : ) + 101 : }) + 102 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| effect-sqlite.ts | +
+ |
+ 16.0 % | +75 | +12 | +|
| sqlite.ts | +
+ |
+ 8.6 % | +175 | +15 | +|
| utils.ts | +
+ |
+ 56.2 % | +32 | +18 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| sqlite.ts | +
+ |
+ 8.6 % | +175 | +15 | +|
| effect-sqlite.ts | +
+ |
+ 16.0 % | +75 | +12 | +|
| utils.ts | +
+ |
+ 56.2 % | +32 | +18 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| effect-sqlite.ts | +
+ |
+ 16.0 % | +75 | +12 | +|
| sqlite.ts | +
+ |
+ 8.6 % | +175 | +15 | +|
| utils.ts | +
+ |
+ 56.2 % | +32 | +18 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 : import type { TablesRelationalConfig } from "drizzle-orm/_relations" + 3 : import type { MigrationMeta } from "drizzle-orm/migrator" + 4 : import type { AnyRelations } from "drizzle-orm/relations" + 5 42 : import { type SQL, sql } from "drizzle-orm/sql/sql" + 6 : import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core" + 7 : import type { SQLiteSession } from "drizzle-orm/sqlite-core/session" + 8 68 : import { GET_VERSION_FOR, MIGRATIONS_TABLE_VERSIONS, type UpgradeResult } from "./utils" + 9 : + 10 : /** @internal */ + 11 : export type SQLiteMigrationTableRow = { id: number | null; hash: string; created_at: number } + 12 : + 13 : type AsyncSQLiteDatabaseWithSession = BaseSQLiteDatabase<"async", unknown, Record<string, unknown>> & { + 14 : session: { + 15 : all<T>(query: SQL): Promise<T[]> + 16 : } + 17 : transaction<T>(transaction: (tx: { run(query: SQL): Promise<unknown> }) => Promise<T>): Promise<T> + 18 : } + 19 : + 20 : type SQLiteMigrationBackfillEntry = { + 21 : name: string + 22 : selector: + 23 : | { column: "id"; value: number } + 24 : | { column: "created_at"; value: number } + 25 : | { column: "hash"; value: string } + 26 : } + 27 : + 28 0 : function unmatchedMigrationError(unmatched: SQLiteMigrationTableRow[]) { + 29 0 : return new Error( + 30 0 : `While upgrading your database migrations table we found ${unmatched.length} (${unmatched + 31 0 : .map((it) => `[id: ${it.id}, created_at: ${it.created_at}]`) + 32 0 : .join( + 33 0 : ", ", + 34 0 : )}) migrations in the database that do not match any local migration. This means that some migrations were applied to the database but are missing from the local environment`, + 35 1 : ) + 36 : } + 37 : + 38 : /** @internal */ + 39 0 : export function prepareSQLiteMigrationBackfill( + 40 0 : dbRows: SQLiteMigrationTableRow[], + 41 0 : localMigrations: MigrationMeta[], + 42 0 : ): SQLiteMigrationBackfillEntry[] { + 43 0 : const sortedLocalMigrations = [...localMigrations].sort((a, b) => + 44 0 : a.folderMillis !== b.folderMillis ? a.folderMillis - b.folderMillis : (a.name ?? "").localeCompare(b.name ?? ""), + 45 0 : ) + 46 0 : const byMillis = new Map<number, MigrationMeta[]>() + 47 0 : const byHash = new Map<string, MigrationMeta>() + 48 0 : for (const migration of sortedLocalMigrations) { + 49 0 : if (!byMillis.has(migration.folderMillis)) { + 50 0 : byMillis.set(migration.folderMillis, []) + 51 0 : } + 52 0 : byMillis.get(migration.folderMillis)!.push(migration) + 53 0 : byHash.set(migration.hash, migration) + 54 0 : } + 55 0 : + 56 0 : const toApply: SQLiteMigrationBackfillEntry[] = [] + 57 0 : const unmatched: SQLiteMigrationTableRow[] = [] + 58 0 : + 59 0 : for (const dbRow of dbRows) { + 60 0 : const stringified = String(dbRow.created_at) + 61 0 : const millis = Number(stringified.substring(0, stringified.length - 3) + "000") + 62 0 : const candidates = byMillis.get(millis) + 63 0 : + 64 0 : const matchedByMillis = candidates?.length === 1 ? candidates[0] : undefined + 65 0 : const matchedByCandidateHash = + 66 0 : candidates && candidates.length > 1 + 67 0 : ? candidates.find((candidate) => candidate.hash && dbRow.hash && candidate.hash === dbRow.hash) + 68 0 : : undefined + 69 0 : const matchedByHash = matchedByMillis || matchedByCandidateHash ? undefined : byHash.get(dbRow.hash) + 70 0 : const matched = matchedByMillis ?? matchedByCandidateHash ?? matchedByHash + 71 0 : + 72 0 : if (matched) { + 73 0 : toApply.push({ + 74 0 : name: matched.name, + 75 0 : selector: + 76 0 : dbRow.id !== null + 77 0 : ? { column: "id", value: dbRow.id } + 78 0 : : matchedByMillis + 79 0 : ? { column: "created_at", value: dbRow.created_at } + 80 0 : : { column: "hash", value: dbRow.hash }, + 81 0 : }) + 82 0 : continue + 83 0 : } + 84 0 : + 85 0 : unmatched.push(dbRow) + 86 0 : } + 87 0 : + 88 0 : if (unmatched.length > 0) { + 89 0 : throw unmatchedMigrationError(unmatched) + 90 0 : } + 91 0 : + 92 1 : return toApply + 93 : } + 94 : + 95 : /** @internal */ + 96 0 : export function buildSQLiteMigrationBackfillStatements( + 97 0 : migrationsTable: string, + 98 0 : backfillEntries: SQLiteMigrationBackfillEntry[], + 99 0 : ) { + 100 0 : const table = sql`${sql.identifier(migrationsTable)}` + 101 0 : const statements: SQL[] = [ + 102 0 : sql`ALTER TABLE ${table} ADD COLUMN ${sql.identifier("name")} text`, + 103 0 : sql`ALTER TABLE ${table} ADD COLUMN ${sql.identifier("applied_at")} TEXT`, + 104 0 : ] + 105 0 : + 106 0 : for (const backfillEntry of backfillEntries) { + 107 0 : const updateQuery = sql`UPDATE ${table} SET ${sql.identifier("name")} = ${backfillEntry.name}, ${sql.identifier( + 108 0 : "applied_at", + 109 0 : )} = NULL WHERE` + 110 0 : + 111 0 : updateQuery.append(sql` ${sql.identifier(backfillEntry.selector.column)} = ${backfillEntry.selector.value}`) + 112 0 : + 113 0 : statements.push(updateQuery) + 114 0 : } + 115 0 : + 116 1 : return statements + 117 : } + 118 : + 119 : /** + 120 : * Detects the current version of the migrations table schema and upgrades it if needed. + 121 : * + 122 : * Version 0: Original schema (id, hash, created_at) + 123 : * Version 1: Extended schema (id, hash, created_at, name, applied_at) + 124 : */ + 125 0 : export function upgradeSyncIfNeeded( + 126 0 : migrationsTable: string, + 127 0 : session: SQLiteSession<"sync", unknown, Record<string, unknown>, AnyRelations, TablesRelationalConfig>, + 128 0 : localMigrations: MigrationMeta[], + 129 0 : ): UpgradeResult { + 130 0 : const tableExists = session.all(sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`) + 131 0 : + 132 0 : if (tableExists.length === 0) { + 133 0 : return { newDb: true } + 134 0 : } + 135 0 : + 136 0 : // Table exists, check table shape + 137 0 : const rows = session.all<{ column_name: string }>( + 138 0 : sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`, + 139 0 : ) + 140 0 : + 141 0 : const version = GET_VERSION_FOR.sqlite(rows.map((r) => r.column_name)) + 142 0 : + 143 0 : for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++) { + 144 0 : const upgradeFn = upgradeSyncFunctions[v] + 145 0 : if (!upgradeFn) { + 146 0 : throw new Error(`No upgrade path from migration table version ${v} to ${v + 1}`) + 147 0 : } + 148 0 : upgradeFn(migrationsTable, session, localMigrations) + 149 0 : } + 150 0 : + 151 1 : return { newDb: false } + 152 : } + 153 : + 154 28 : const upgradeSyncFunctions: Record< + 155 : number, + 156 : ( + 157 : migrationsTable: string, + 158 : session: SQLiteSession<"sync", unknown, Record<string, unknown>, AnyRelations, TablesRelationalConfig>, + 159 : localMigrations: MigrationMeta[], + 160 : ) => void + 161 3 : > = { + 162 : /** + 163 : * Upgrade from version 0 to version 1: + 164 : * 1. Read all existing DB migrations + 165 : * 2. Sort localMigrations ASC by millis and if the same - sort by name + 166 : * 3. Match each DB row to a local migration + 167 : * If multiple migrations share the same second, use hash matching as a tiebreaker + 168 : * Not implemented for now -> If hash matching fails, fall back to serial id ordering + 169 : * 5. Create extra column and backfill names for matched migrations + 170 : */ + 171 0 : 0: (migrationsTable, session, localMigrations) => { + 172 0 : const table = sql`${sql.identifier(migrationsTable)}` + 173 0 : const dbRows = session.all<SQLiteMigrationTableRow>(sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`) + 174 0 : const statements = buildSQLiteMigrationBackfillStatements( + 175 0 : migrationsTable, + 176 0 : prepareSQLiteMigrationBackfill(dbRows, localMigrations), + 177 0 : ) + 178 0 : + 179 0 : session.transaction((tx) => { + 180 0 : for (const statement of statements) { + 181 0 : tx.run(statement) + 182 0 : } + 183 1 : }) + 184 : }, + 185 2 : } + 186 : + 187 : /** + 188 : * Detects the current version of the migrations table schema and upgrades it if needed. + 189 : * + 190 : * Version 0: Original schema (id, hash, created_at) + 191 : * Version 1: Extended schema (id, hash, created_at, name, applied_at) + 192 : */ + 193 0 : export async function upgradeAsyncIfNeeded( + 194 0 : migrationsTable: string, + 195 0 : db: AsyncSQLiteDatabaseWithSession, + 196 0 : localMigrations: MigrationMeta[], + 197 0 : ): Promise<UpgradeResult> { + 198 0 : // Check if the table exists at all + 199 0 : const tableExists = await db.session.all( + 200 0 : sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`, + 201 0 : ) + 202 0 : + 203 0 : if (tableExists.length === 0) { + 204 0 : return { newDb: true } + 205 0 : } + 206 0 : + 207 0 : const rows = await db.session.all<{ column_name: string }>( + 208 0 : sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`, + 209 0 : ) + 210 0 : + 211 0 : const version = GET_VERSION_FOR.sqlite(rows.map((r) => r.column_name)) + 212 0 : + 213 0 : for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++) { + 214 0 : const upgradeFn = upgradeAsyncFunctions[v] + 215 0 : if (!upgradeFn) { + 216 0 : throw new Error(`No upgrade path from migration table version ${v} to ${v + 1}`) + 217 0 : } + 218 0 : await upgradeFn(migrationsTable, db, localMigrations) + 219 0 : } + 220 0 : + 221 1 : return { newDb: false } + 222 : } + 223 : + 224 29 : const upgradeAsyncFunctions: Record< + 225 : number, + 226 : (migrationsTable: string, db: AsyncSQLiteDatabaseWithSession, localMigrations: MigrationMeta[]) => Promise<void> + 227 3 : > = { + 228 : /** + 229 : * Upgrade from version 0 to version 1: + 230 : * 1. Read all existing DB migrations + 231 : * 2. Sort localMigrations ASC by millis and if the same - sort by name + 232 : * 3. Match each DB row to a local migration + 233 : * If multiple migrations share the same second, use hash matching as a tiebreaker + 234 : * Not implemented for now -> If hash matching fails, fall back to serial id ordering + 235 : * 5. Create extra column and backfill names for matched migrations + 236 : */ + 237 0 : 0: async (migrationsTable, db, localMigrations) => { + 238 0 : const table = sql`${sql.identifier(migrationsTable)}` + 239 0 : const dbRows = await db.session.all<SQLiteMigrationTableRow>( + 240 0 : sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`, + 241 0 : ) + 242 0 : const statements = buildSQLiteMigrationBackfillStatements( + 243 0 : migrationsTable, + 244 0 : prepareSQLiteMigrationBackfill(dbRows, localMigrations), + 245 0 : ) + 246 0 : + 247 0 : await db.transaction(async (tx) => { + 248 0 : for (const statement of statements) { + 249 0 : await tx.run(statement) + 250 0 : } + 251 1 : }) + 252 : }, + 253 1 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /* oxlint-disable */ + 2 : export interface UpgradeResult { + 3 : newDb: boolean + 4 : } + 5 : + 6 43 : export const MIGRATIONS_TABLE_VERSIONS = { + 7 12 : sqlite: 1, + 8 8 : pg: 1, + 9 12 : effect: 1, + 10 11 : mysql: 1, + 11 11 : mssql: 1, + 12 15 : cockroach: 1, + 13 15 : singlestore: 1, + 14 2 : } as const + 15 : + 16 33 : export const GET_VERSION_FOR = { + 17 0 : mysql: (columns: string[]): number => { + 18 0 : if (columns.includes("name")) return 1 + 19 3 : return 0 + 20 : }, + 21 0 : pg: (columns: string[]): number => { + 22 0 : if (columns.includes("name")) return 1 + 23 3 : return 0 + 24 : }, + 25 0 : effect: (columns: string[]): number => { + 26 0 : if (columns.includes("name")) return 1 + 27 3 : return 0 + 28 : }, + 29 0 : mssql: (columns: string[]): number => { + 30 0 : if (columns.includes("name")) return 1 + 31 3 : return 0 + 32 : }, + 33 0 : cockroach: (columns: string[]): number => { + 34 0 : if (columns.includes("name")) return 1 + 35 3 : return 0 + 36 : }, + 37 0 : singlestore: (columns: string[]): number => { + 38 0 : if (columns.includes("name")) return 1 + 39 3 : return 0 + 40 : }, + 41 0 : sqlite: (columns: string[]): number => { + 42 0 : if (columns.includes("name")) return 1 + 43 1 : return 0 + 44 : }, + 45 1 : } as const ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 41 : import { Context, Effect, FileSystem, Layer, Schema, Semaphore } from "effect" + 2 30 : import * as fs from "node:fs" + 3 34 : import * as path from "node:path" + 4 69 : import { secretFindings, SecretFindingSchema, type SecretFinding } from "./redaction.js" + 5 61 : import { CassetteSchema, encodeCassette, type Cassette, type CassetteMetadata, type Interaction } from "./schema.js" + 6 : + 7 93 : const DEFAULT_RECORDINGS_DIR = path.resolve(process.cwd(), "test", "fixtures", "recordings") + 8 : + 9 96 : export class CassetteNotFoundError extends Schema.TaggedErrorClass<CassetteNotFoundError>()("CassetteNotFoundError", { + 10 28 : cassetteName: Schema.String, + 11 0 : }) { + 12 0 : override get message() { + 13 1 : return `Cassette "${this.cassetteName}" not found` + 14 : } + 15 1 : } + 16 : + 17 92 : export class UnsafeCassetteError extends Schema.TaggedErrorClass<UnsafeCassetteError>()("UnsafeCassetteError", { + 18 30 : cassetteName: Schema.String, + 19 44 : findings: Schema.Array(SecretFindingSchema), + 20 0 : }) { + 21 0 : override get message() { + 22 0 : return `Refusing to write cassette "${this.cassetteName}" because it contains possible secrets: ${this.findings + 23 0 : .map((finding) => `${finding.path} (${finding.reason})`) + 24 1 : .join(", ")}` + 25 : } + 26 1 : } + 27 : + 28 : export interface Interface { + 29 : readonly read: (name: string) => Effect.Effect<ReadonlyArray<Interaction>, CassetteNotFoundError> + 30 : readonly append: ( + 31 : name: string, + 32 : interaction: Interaction, + 33 : metadata?: CassetteMetadata, + 34 : ) => Effect.Effect<void, UnsafeCassetteError> + 35 : readonly exists: (name: string) => Effect.Effect<boolean> + 36 : readonly list: () => Effect.Effect<ReadonlyArray<string>> + 37 : } + 38 : + 39 88 : export class Service extends Context.Service<Service, Interface>()("@opencode-ai/http-recorder/Cassette") {} + 40 : + 41 43 : const cassettePath = (directory: string, name: string) => { + 42 107 : if (!name || path.isAbsolute(name) || path.win32.isAbsolute(name) || name.split(/[\\/]/).includes("..")) + 43 2 : throw new Error(`Invalid cassette name "${name}"`) + 44 39 : const root = path.resolve(directory) + 45 52 : const target = path.resolve(root, `${name}.json`) + 46 47 : const relative = path.relative(root, target) + 47 75 : if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) + 48 2 : throw new Error(`Invalid cassette name "${name}"`) + 49 15 : return target + 50 : } + 51 : + 52 0 : export const hasCassetteSync = (name: string, options: { readonly directory?: string } = {}) => + 53 2 : fs.existsSync(cassettePath(options.directory ?? DEFAULT_RECORDINGS_DIR, name)) + 54 : + 55 0 : const buildCassette = ( + 56 0 : name: string, + 57 0 : interactions: ReadonlyArray<Interaction>, + 58 0 : metadata: CassetteMetadata | undefined, + 59 0 : ): Cassette => ({ + 60 0 : version: 1, + 61 0 : metadata: { name, recordedAt: new Date().toISOString(), ...metadata }, + 62 : interactions, + 63 2 : }) + 64 : + 65 23 : const formatCassette = (cassette: Cassette) => `${JSON.stringify(encodeCassette(cassette), null, 2)}\n` + 66 : + 67 86 : const parseCassette = Schema.decodeUnknownSync(Schema.fromJsonString(CassetteSchema)) + 68 : + 69 0 : const failIfUnsafe = (name: string, findings: ReadonlyArray<SecretFinding>) => + 70 2 : findings.length === 0 ? Effect.void : Effect.fail(new UnsafeCassetteError({ cassetteName: name, findings })) + 71 : + 72 25 : export const fileSystem = ( + 73 17 : options: { readonly directory?: string } = {}, + 74 : ): Layer.Layer<Service, never, FileSystem.FileSystem> => + 75 13 : Layer.effect( + 76 9 : Service, + 77 15 : Effect.gen(function* () { + 78 42 : const fs = yield* FileSystem.FileSystem + 79 64 : const directory = options.directory ?? DEFAULT_RECORDINGS_DIR + 80 27 : const recorded = new Map<string, { interactions: Interaction[]; findings: SecretFinding[] }>() + 81 46 : const appendLock = yield* Semaphore.make(1) + 82 : + 83 56 : const pathFor = (name: string) => cassettePath(directory, name) + 84 : + 85 0 : const walk = (current: string): Effect.Effect<ReadonlyArray<string>> => + 86 0 : Effect.gen(function* () { + 87 0 : const entries = yield* fs.readDirectory(current).pipe(Effect.catch(() => Effect.succeed([] as string[]))) + 88 0 : const nested = yield* Effect.forEach(entries, (entry) => { + 89 0 : const full = path.join(current, entry) + 90 0 : return fs.stat(full).pipe( + 91 0 : Effect.flatMap((stat) => (stat.type === "Directory" ? walk(full) : Effect.succeed([full]))), + 92 0 : Effect.catch(() => Effect.succeed([] as string[])), + 93 0 : ) + 94 0 : }) + 95 : return nested.flat() + 96 3 : }) + 97 : + 98 23 : return Service.of({ + 99 15 : read: (name) => + 100 38 : fs.readFileString(pathFor(name)).pipe( + 101 52 : Effect.map((raw) => parseCassette(raw).interactions), + 102 13 : Effect.catch(() => Effect.fail(new CassetteNotFoundError({ cassetteName: name }))), + 103 5 : ), + 104 0 : append: (name, interaction, metadata) => + 105 0 : appendLock.withPermit( + 106 0 : Effect.gen(function* () { + 107 0 : const entry = recorded.get(name) ?? { interactions: [], findings: [] } + 108 0 : const interactions = [...entry.interactions, interaction] + 109 0 : const interactionFindings = [...entry.findings, ...secretFindings(interaction)] + 110 0 : const cassette = buildCassette(name, interactions, metadata) + 111 0 : const findings = [...interactionFindings, ...secretFindings(cassette.metadata ?? {})] + 112 0 : yield* failIfUnsafe(name, findings) + 113 0 : const target = pathFor(name) + 114 0 : yield* fs.makeDirectory(path.dirname(target), { recursive: true }).pipe(Effect.orDie) + 115 0 : const temporary = `${target}.${crypto.randomUUID()}.tmp` + 116 0 : yield* fs.writeFileString(temporary, formatCassette(cassette)).pipe( + 117 0 : Effect.flatMap(() => fs.rename(temporary, target)), + 118 0 : Effect.ensuring(fs.remove(temporary, { force: true }).pipe(Effect.catch(() => Effect.void))), + 119 0 : Effect.orDie, + 120 0 : ) + 121 0 : recorded.set(name, { interactions, findings: interactionFindings }) + 122 : }), + 123 5 : ), + 124 17 : exists: (name) => + 125 30 : fs.access(pathFor(name)).pipe( + 126 17 : Effect.as(true), + 127 13 : Effect.catch(() => Effect.succeed(false)), + 128 5 : ), + 129 0 : list: () => + 130 0 : walk(directory).pipe( + 131 0 : Effect.map((files) => + 132 0 : files + 133 0 : .filter((file) => file.endsWith(".json")) + 134 0 : .map((file) => + 135 0 : path + 136 0 : .relative(directory, file) + 137 0 : .replace(/\\/g, "/") + 138 0 : .replace(/\.json$/, ""), + 139 0 : ) + 140 0 : .toSorted((a, b) => a.localeCompare(b)), + 141 : ), + 142 2 : ), + 143 2 : }) + 144 1 : }), + 145 2 : ) + 146 : + 147 0 : export const memory = (initial: Record<string, ReadonlyArray<Interaction>> = {}): Layer.Layer<Service> => + 148 0 : Layer.sync(Service, () => { + 149 0 : const stored = new Map<string, Interaction[]>( + 150 0 : Object.entries(initial).map(([name, interactions]) => [name, [...interactions]]), + 151 0 : ) + 152 0 : const accumulatedFindings = new Map<string, SecretFinding[]>() + 153 0 : const appendLock = Semaphore.makeUnsafe(1) + 154 0 : + 155 0 : return Service.of({ + 156 0 : read: (name) => + 157 0 : stored.has(name) + 158 0 : ? Effect.succeed(stored.get(name) ?? []) + 159 0 : : Effect.fail(new CassetteNotFoundError({ cassetteName: name })), + 160 0 : append: (name, interaction, metadata) => + 161 0 : appendLock.withPermit( + 162 0 : Effect.suspend(() => { + 163 0 : const interactions = [...(stored.get(name) ?? []), interaction] + 164 0 : const findings = [...(accumulatedFindings.get(name) ?? []), ...secretFindings(interaction)] + 165 0 : const allFindings = metadata ? [...findings, ...secretFindings({ name, ...metadata })] : findings + 166 0 : return failIfUnsafe(name, allFindings).pipe( + 167 0 : Effect.tap(() => + 168 0 : Effect.sync(() => { + 169 0 : stored.set(name, interactions) + 170 0 : accumulatedFindings.set(name, findings) + 171 0 : }), + 172 0 : ), + 173 0 : ) + 174 0 : }), + 175 0 : ), + 176 0 : exists: (name) => Effect.sync(() => stored.has(name)), + 177 0 : list: () => Effect.sync(() => Array.from(stored.keys()).toSorted()), + 178 : }) + 179 1 : }) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 55 : import { NodeFileSystem } from "@effect/platform-node" + 2 38 : import * as Layer from "effect/Layer" + 3 55 : import { FetchHttpClient } from "effect/unstable/http" + 4 : import type * as HttpClient from "effect/unstable/http/HttpClient" + 5 49 : import * as CassetteService from "./cassette.js" + 6 54 : import { recordingLayer } from "./internal-effect.js" + 7 37 : import { make } from "./redactor.js" + 8 : import type { RecorderOptions } from "./types.js" + 9 : + 10 : /** + 11 : * Provides a fetch-backed `HttpClient` with cassette recording and replay. + 12 : * + 13 : * Locally, a missing cassette is recorded from the real service. Existing + 14 : * cassettes are replayed, and `CI=true` makes a missing cassette fail. + 15 : */ + 16 42 : export const http = (name: string, options: RecorderOptions = {}): Layer.Layer<HttpClient.HttpClient> => + 17 24 : recordingLayer(name, { + 18 29 : metadata: options.metadata, + 19 33 : redactor: make(options.redact), + 20 21 : match: options.match, + 21 7 : }).pipe( + 22 77 : Layer.provide(CassetteService.fileSystem({ directory: options.directory })), + 23 38 : Layer.provide(FetchHttpClient.layer), + 24 35 : Layer.provide(NodeFileSystem.layer), + 25 1 : ) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| cassette.ts | +
+ |
+ 40.3 % | +144 | +58 | +|
| effect.ts | +
+ |
+ 100.0 % | +16 | +16 | +|
| index.ts | +
+ |
+ 100.0 % | +3 | +3 | +|
| internal-effect.ts | +
+ |
+ 54.2 % | +144 | +78 | +|
| internal.ts | +
+ |
+ 100.0 % | +8 | +8 | +|
| matching.ts | +
+ |
+ 51.2 % | +86 | +44 | +|
| recorder.ts | +
+ |
+ 95.2 % | +42 | +40 | +|
| redaction.ts | +
+ |
+ 76.0 % | +96 | +73 | +|
| redactor.ts | +
+ |
+ 80.2 % | +91 | +73 | +|
| schema.ts | +
+ |
+ 98.2 % | +55 | +54 | +|
| socket.ts | +
+ |
+ 8.1 % | +270 | +22 | +|
| websocket.ts | +
+ |
+ 8.1 % | +135 | +11 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| websocket.ts | +
+ |
+ 8.1 % | +135 | +11 | +|
| socket.ts | +
+ |
+ 8.1 % | +270 | +22 | +|
| cassette.ts | +
+ |
+ 40.3 % | +144 | +58 | +|
| matching.ts | +
+ |
+ 51.2 % | +86 | +44 | +|
| internal-effect.ts | +
+ |
+ 54.2 % | +144 | +78 | +|
| redaction.ts | +
+ |
+ 76.0 % | +96 | +73 | +|
| redactor.ts | +
+ |
+ 80.2 % | +91 | +73 | +|
| recorder.ts | +
+ |
+ 95.2 % | +42 | +40 | +|
| schema.ts | +
+ |
+ 98.2 % | +55 | +54 | +|
| index.ts | +
+ |
+ 100.0 % | +3 | +3 | +|
| internal.ts | +
+ |
+ 100.0 % | +8 | +8 | +|
| effect.ts | +
+ |
+ 100.0 % | +16 | +16 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| cassette.ts | +
+ |
+ 40.3 % | +144 | +58 | +|
| effect.ts | +
+ |
+ 100.0 % | +16 | +16 | +|
| index.ts | +
+ |
+ 100.0 % | +3 | +3 | +|
| internal-effect.ts | +
+ |
+ 54.2 % | +144 | +78 | +|
| internal.ts | +
+ |
+ 100.0 % | +8 | +8 | +|
| matching.ts | +
+ |
+ 51.2 % | +86 | +44 | +|
| recorder.ts | +
+ |
+ 95.2 % | +42 | +40 | +|
| redaction.ts | +
+ |
+ 76.0 % | +96 | +73 | +|
| redactor.ts | +
+ |
+ 80.2 % | +91 | +73 | +|
| schema.ts | +
+ |
+ 98.2 % | +55 | +54 | +|
| socket.ts | +
+ |
+ 8.1 % | +270 | +22 | +|
| websocket.ts | +
+ |
+ 8.1 % | +135 | +11 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 35 : import { http } from "./effect.js" + 2 37 : import { socket } from "./socket.js" + 3 : + 4 : /** HTTP and WebSocket cassette recording. */ + 5 44 : export const HttpRecorder = { http, socket } as const + 6 : + 7 : export namespace HttpRecorder { + 8 : /** Additional JSON metadata stored with a cassette. */ + 9 : export type CassetteMetadata = import("./types.js").CassetteMetadata + 10 : /** Recorder configuration. */ + 11 : export type RecorderOptions = import("./types.js").RecorderOptions + 12 : /** Additive redaction and header-preservation policy. */ + 13 : export type RedactOptions = import("./types.js").RedactOptions + 14 : /** Returns whether an incoming HTTP request matches a recorded request. */ + 15 : export type RequestMatcher = import("./types.js").RequestMatcher + 16 : /** The normalized HTTP request representation used for matching. */ + 17 : export type RequestSnapshot = import("./types.js").RequestSnapshot + 18 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 55 : import { NodeFileSystem } from "@effect/platform-node" + 2 62 : import { Deferred, Effect, Layer, Option, Ref } from "effect" + 3 160 : import { + 4 : FetchHttpClient, + 5 : Headers, + 6 : HttpBody, + 7 : HttpClient, + 8 : HttpClientError, + 9 : HttpClientRequest, + 10 : HttpClientResponse, + 11 : UrlParams, + 12 : } from "effect/unstable/http" + 13 49 : import * as CassetteService from "./cassette.js" + 14 65 : import { defaultMatcher, selectSequential } from "./matching.js" + 15 65 : import { makeReplayState, resolveAutoMode } from "./recorder.js" + 16 37 : import { make, type Redactor } from "./redactor.js" + 17 43 : import { redactUrl } from "./redaction.js" + 18 47 : import { httpInteractions } from "./schema.js" + 19 : import type { CassetteMetadata, HttpInteraction, RequestMatcher, ResponseSnapshot } from "./types.js" + 20 : + 21 26 : export { defaultMatcher } + 22 : + 23 : export type RecordReplayMode = "auto" | "record" | "replay" | "passthrough" + 24 : + 25 : export interface RecordReplayOptions { + 26 : readonly mode?: RecordReplayMode + 27 : readonly directory?: string + 28 : readonly metadata?: CassetteMetadata + 29 : readonly redactor?: Redactor + 30 : readonly match?: RequestMatcher + 31 : } + 32 : + 33 37 : const TEXT_CONTENT_TYPES = new Set([ + 34 24 : "application/graphql", + 35 27 : "application/javascript", + 36 21 : "application/json", + 37 20 : "application/sql", + 38 38 : "application/x-www-form-urlencoded", + 39 20 : "application/xml", + 40 21 : "application/yaml", + 41 16 : "image/svg+xml", + 42 3 : ]) + 43 : + 44 0 : const isTextContentType = (contentType: string | undefined) => { + 45 0 : const mediaType = contentType?.split(";", 1)[0]?.trim().toLowerCase() + 46 0 : if (!mediaType) return false + 47 0 : return ( + 48 0 : mediaType.startsWith("text/") || + 49 0 : mediaType.endsWith("+json") || + 50 0 : mediaType.endsWith("+xml") || + 51 2 : TEXT_CONTENT_TYPES.has(mediaType) + 52 : ) + 53 : } + 54 : + 55 0 : const captureResponseBody = (response: HttpClientResponse.HttpClientResponse, contentType: string | undefined) => + 56 0 : response.arrayBuffer.pipe( + 57 0 : Effect.map((bytes) => + 58 0 : isTextContentType(contentType) + 59 0 : ? { body: new TextDecoder().decode(bytes) } + 60 0 : : { body: Buffer.from(bytes).toString("base64"), bodyEncoding: "base64" as const }, + 61 : ), + 62 2 : ) + 63 : + 64 39 : const decodeResponseBody = (snapshot: ResponseSnapshot) => + 65 53 : snapshot.bodyEncoding === "base64" ? Buffer.from(snapshot.body, "base64") : snapshot.body + 66 : + 67 50 : const responseFromSnapshot = (request: HttpClientRequest.HttpClientRequest, snapshot: ResponseSnapshot) => + 68 27 : HttpClientResponse.fromWeb( + 69 9 : request, + 70 13 : new Response( + 71 108 : request.method === "HEAD" || snapshot.status === 204 || snapshot.status === 205 || snapshot.status === 304 + 72 2 : ? null + 73 29 : : decodeResponseBody(snapshot), + 74 8 : snapshot, + 75 1 : ), + 76 2 : ) + 77 : + 78 0 : export const redactedErrorRequest = (request: HttpClientRequest.HttpClientRequest) => + 79 0 : HttpClientRequest.makeWith( + 80 0 : request.method, + 81 0 : redactUrl(request.url), + 82 0 : UrlParams.empty, + 83 0 : Option.none(), + 84 0 : Headers.empty, + 85 : HttpBody.empty, + 86 2 : ) + 87 : + 88 0 : const transportError = (request: HttpClientRequest.HttpClientRequest, description: string) => + 89 0 : new HttpClientError.HttpClientError({ + 90 : reason: new HttpClientError.TransportError({ request: redactedErrorRequest(request), description }), + 91 2 : }) + 92 : + 93 29 : export const recordingLayer = ( + 94 6 : name: string, + 95 17 : options: Omit<RecordReplayOptions, "directory"> = {}, + 96 : ): Layer.Layer<HttpClient.HttpClient, never, HttpClient.HttpClient | CassetteService.Service> => + 97 13 : Layer.effect( + 98 23 : HttpClient.HttpClient, + 99 15 : Effect.gen(function* () { + 100 48 : const upstream = yield* HttpClient.HttpClient + 101 57 : const cassetteService = yield* CassetteService.Service + 102 46 : const redactor = options.redactor ?? make() + 103 48 : const match = options.match ?? defaultMatcher + 104 43 : const requested = options.mode ?? "auto" + 105 82 : const mode = requested === "auto" ? yield* resolveAutoMode(cassetteService, name) : requested + 106 : + 107 36 : const snapshotRequest = (request: HttpClientRequest.HttpClientRequest) => + 108 17 : Effect.gen(function* () { + 109 75 : const web = yield* HttpClientRequest.toWeb(request).pipe(Effect.orDie) + 110 31 : return redactor.request({ + 111 25 : method: web.method, + 112 19 : url: web.url, + 113 57 : headers: Object.fromEntries(web.headers.entries()), + 114 47 : body: yield* Effect.promise(() => web.text()), + 115 3 : }) + 116 3 : }) + 117 : + 118 32 : if (mode === "passthrough") return upstream + 119 : + 120 22 : if (mode === "record") { + 121 0 : const initial = yield* Deferred.make<void>() + 122 0 : yield* Deferred.succeed(initial, undefined) + 123 0 : const tail = yield* Ref.make(initial) + 124 0 : return HttpClient.make((request) => + 125 0 : Effect.gen(function* () { + 126 0 : const completed = yield* Deferred.make<void>() + 127 0 : const previous = yield* Ref.modify(tail, (current) => [current, completed]) + 128 0 : return yield* Effect.gen(function* () { + 129 0 : const incoming = yield* snapshotRequest(request) + 130 0 : const response = yield* upstream.execute(request) + 131 0 : const captured = yield* captureResponseBody(response, response.headers["content-type"]) + 132 0 : const responseSnapshot: ResponseSnapshot = { + 133 0 : status: response.status, + 134 0 : headers: response.headers as Record<string, string>, + 135 0 : ...captured, + 136 0 : } + 137 0 : const interaction: HttpInteraction = { + 138 0 : transport: "http", + 139 0 : request: incoming, + 140 0 : response: redactor.response(responseSnapshot), + 141 0 : } + 142 0 : yield* Deferred.await(previous) + 143 0 : yield* cassetteService + 144 0 : .append(name, interaction, options.metadata) + 145 0 : .pipe( + 146 0 : Effect.catchTag("UnsafeCassetteError", (error) => + 147 0 : Effect.fail(transportError(request, error.message)), + 148 0 : ), + 149 0 : ) + 150 0 : return responseFromSnapshot(request, responseSnapshot) + 151 : }).pipe(Effect.ensuring(Deferred.succeed(completed, undefined))) + 152 : }), + 153 0 : ) + 154 2 : } + 155 : + 156 81 : const replay = yield* makeReplayState(cassetteService, name, httpInteractions) + 157 35 : return HttpClient.make((request) => + 158 17 : Effect.gen(function* () { + 159 53 : const incoming = yield* snapshotRequest(request) + 160 30 : const claimed = yield* replay + 161 50 : .claim((interaction, index, interactions) => { + 162 76 : const result = selectSequential(interactions, incoming, match, index) + 163 48 : if (result.interaction) return Effect.void + 164 0 : return Effect.fail( + 165 0 : transportError(request, `Fixture "${name}" does not match the current request: ${result.detail}.`), + 166 0 : ) + 167 2 : }) + 168 5 : .pipe( + 169 0 : Effect.mapError((error) => + 170 0 : error._tag === "CassetteNotFoundError" + 171 0 : ? transportError( + 172 0 : request, + 173 0 : `Fixture "${name}" not found. Run locally to record it (CI=true forces replay).`, + 174 0 : ) + 175 : : error, + 176 1 : ), + 177 6 : ) + 178 67 : return responseFromSnapshot(request, claimed.interaction.response) + 179 : }), + 180 1 : ) + 181 1 : }), + 182 2 : ) + 183 : + 184 0 : export const cassetteLayer = (name: string, options: RecordReplayOptions = {}): Layer.Layer<HttpClient.HttpClient> => + 185 0 : recordingLayer(name, options).pipe( + 186 0 : Layer.provide(CassetteService.fileSystem({ directory: options.directory })), + 187 0 : Layer.provide(FetchHttpClient.layer), + 188 : Layer.provide(NodeFileSystem.layer), + 189 1 : ) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 92 : export { CassetteNotFoundError, hasCassetteSync, UnsafeCassetteError } from "./cassette.js" + 2 69 : export { cassetteLayer, recordingLayer, type RecordReplayMode, type RecordReplayOptions } from "./internal-effect.js" + 3 74 : export { redactHeaders, redactUrl, secretFindings, type SecretFinding } from "./redaction.js" + 4 42 : export { socketLayer } from "./socket.js" + 5 55 : export { + 6 : makeWebSocketExecutor, + 7 : type WebSocketConnection, + 8 : type WebSocketExecutor, + 9 : type WebSocketRecordReplayOptions, + 10 : type WebSocketRequest, + 11 : } from "./websocket.js" + 12 42 : export * as Cassette from "./cassette.js" + 13 42 : export * as Redactor from "./redactor.js" + 14 : + 15 53 : export * as HttpRecorderInternal from "./internal.js" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 40 : import { Option, Schema } from "effect" + 2 58 : import { REDACTED, secretFindings } from "./redaction.js" + 3 : import type { HttpInteraction, RequestMatcher, RequestSnapshot } from "./types.js" + 4 : + 5 56 : const JsonValue = Schema.fromJsonString(Schema.Unknown) + 6 64 : export const decodeJson = Schema.decodeUnknownOption(JsonValue) + 7 : + 8 26 : const isRecord = (value: unknown): value is Record<string, unknown> => + 9 69 : value !== null && typeof value === "object" && !Array.isArray(value) + 10 : + 11 44 : export const canonicalizeJson = (value: unknown): unknown => { + 12 64 : if (Array.isArray(value)) return value.map(canonicalizeJson) + 13 25 : if (isRecord(value)) { + 14 26 : return Object.fromEntries( + 15 19 : Object.keys(value) + 16 11 : .toSorted() + 17 47 : .map((key) => [key, canonicalizeJson(value[key])]), + 18 1 : ) + 19 2 : } + 20 14 : return value + 21 : } + 22 : + 23 : export type { RequestMatcher } from "./types.js" + 24 : + 25 45 : export const canonicalSnapshot = (snapshot: RequestSnapshot): string => + 26 18 : JSON.stringify({ + 27 26 : method: snapshot.method, + 28 20 : url: snapshot.url, + 29 46 : headers: canonicalizeJson(snapshot.headers), + 30 51 : body: Option.match(decodeJson(snapshot.body), { + 31 12 : onNone: () => snapshot.body, + 32 26 : onSome: canonicalizeJson, + 33 3 : }), + 34 2 : }) + 35 : + 36 52 : export const defaultMatcher: RequestMatcher = (incoming, recorded) => + 37 60 : canonicalSnapshot(incoming) === canonicalSnapshot(recorded) + 38 : + 39 0 : export const safeText = (value: unknown) => { + 40 0 : if (value === undefined) return "undefined" + 41 0 : if (secretFindings(value).length > 0) return JSON.stringify(REDACTED) + 42 0 : const text = JSON.stringify(value) + 43 0 : if (!text) return typeof value + 44 2 : return text.length > 300 ? `${text.slice(0, 300)}...` : text + 45 : } + 46 : + 47 17 : const jsonBody = (body: string) => Option.getOrUndefined(decodeJson(body)) + 48 : + 49 0 : const valueDiffs = (expected: unknown, received: unknown, base = "$", limit = 8): ReadonlyArray<string> => { + 50 0 : if (Object.is(expected, received)) return [] + 51 0 : if (isRecord(expected) && isRecord(received)) { + 52 0 : return [...new Set([...Object.keys(expected), ...Object.keys(received)])] + 53 0 : .toSorted() + 54 0 : .flatMap((key) => valueDiffs(expected[key], received[key], `${base}.${key}`, limit)) + 55 0 : .slice(0, limit) + 56 0 : } + 57 0 : if (Array.isArray(expected) && Array.isArray(received)) { + 58 0 : return Array.from({ length: Math.max(expected.length, received.length) }, (_, index) => index) + 59 0 : .flatMap((index) => valueDiffs(expected[index], received[index], `${base}[${index}]`, limit)) + 60 0 : .slice(0, limit) + 61 0 : } + 62 2 : return [`${base} expected ${safeText(expected)}, received ${safeText(received)}`] + 63 : } + 64 : + 65 0 : const headerDiffs = (expected: Record<string, string>, received: Record<string, string>) => + 66 0 : [...new Set([...Object.keys(expected), ...Object.keys(received)])].toSorted().flatMap((key) => { + 67 0 : if (expected[key] === received[key]) return [] + 68 0 : if (expected[key] === undefined) return [` ${key} unexpected ${safeText(received[key])}`] + 69 0 : if (received[key] === undefined) return [` ${key} missing expected ${safeText(expected[key])}`] + 70 : return [` ${key} expected ${safeText(expected[key])}, received ${safeText(received[key])}`] + 71 2 : }) + 72 : + 73 0 : export const requestDiff = (expected: RequestSnapshot, received: RequestSnapshot): ReadonlyArray<string> => { + 74 0 : const lines: string[] = [] + 75 0 : if (expected.method !== received.method) { + 76 0 : lines.push("method:", ` expected ${expected.method}, received ${received.method}`) + 77 0 : } + 78 0 : if (expected.url !== received.url) { + 79 0 : lines.push("url:", ` expected ${expected.url}`, ` received ${received.url}`) + 80 0 : } + 81 0 : const headers = headerDiffs(expected.headers, received.headers) + 82 0 : if (headers.length > 0) lines.push("headers:", ...headers.slice(0, 8)) + 83 0 : const expectedBody = jsonBody(expected.body) + 84 0 : const receivedBody = jsonBody(received.body) + 85 0 : const body = + 86 0 : expectedBody !== undefined && receivedBody !== undefined + 87 0 : ? valueDiffs(expectedBody, receivedBody).map((line) => ` ${line}`) + 88 0 : : expected.body === received.body + 89 0 : ? [] + 90 0 : : [` expected ${safeText(expected.body)}, received ${safeText(received.body)}`] + 91 0 : if (body.length > 0) lines.push("body:", ...body) + 92 2 : return lines + 93 : } + 94 : + 95 31 : export const selectSequential = ( + 96 14 : interactions: ReadonlyArray<HttpInteraction>, + 97 10 : incoming: RequestSnapshot, + 98 7 : match: RequestMatcher, + 99 10 : index: number, + 100 3 : ): { readonly interaction: HttpInteraction | undefined; readonly detail: string } => { + 101 42 : const interaction = interactions[index] + 102 22 : if (!interaction) return { interaction, detail: `interaction ${index + 1} of ${interactions.length} not recorded` } + 103 45 : if (!match(incoming, interaction.request)) + 104 2 : return { interaction: undefined, detail: requestDiff(interaction.request, incoming).join("\n") } + 105 35 : return { interaction, detail: "" } + 106 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 49 : import { Effect, Scope, SynchronizedRef } from "effect" + 2 : import type * as CassetteService from "./cassette.js" + 3 : import type { CassetteNotFoundError } from "./cassette.js" + 4 : import type { Interaction } from "./schema.js" + 5 : + 6 20 : const isCI = () => { + 7 31 : const value = process.env.CI + 8 82 : return value !== undefined && value !== "" && value !== "false" && value !== "0" + 9 : } + 10 : + 11 30 : export const resolveAutoMode = ( + 12 10 : cassette: CassetteService.Interface, + 13 9 : name: string, + 14 : ): Effect.Effect<"record" | "replay" | "passthrough"> => + 15 15 : Effect.gen(function* () { + 16 16 : if (isCI()) return "replay" + 17 47 : return (yield* cassette.exists(name)) ? "replay" : "record" + 18 2 : }) + 19 : + 20 : export interface ReplayState<T> { + 21 : readonly claim: <E>( + 22 : validate: (interaction: T | undefined, index: number, interactions: ReadonlyArray<T>) => Effect.Effect<void, E>, + 23 : ) => Effect.Effect<{ readonly interaction: T; readonly index: number }, CassetteNotFoundError | E> + 24 : } + 25 : + 26 30 : export const makeReplayState = <T>( + 27 10 : cassette: CassetteService.Interface, + 28 6 : name: string, + 29 12 : project: (interactions: ReadonlyArray<Interaction>) => ReadonlyArray<T>, + 30 : ): Effect.Effect<ReplayState<T>, never, Scope.Scope> => + 31 15 : Effect.gen(function* () { + 32 83 : const load = yield* Effect.cached(cassette.read(name).pipe(Effect.map(project))) + 33 50 : const position = yield* SynchronizedRef.make(0) + 34 : + 35 32 : yield* Effect.addFinalizer(() => + 36 17 : Effect.gen(function* () { + 37 54 : const used = yield* SynchronizedRef.get(position) + 38 24 : if (used === 0) return yield* Effect.void + 39 56 : const interactions = yield* load.pipe(Effect.orDie) + 40 36 : if (used < interactions.length) + 41 0 : return yield* Effect.die( + 42 0 : new Error(`Unused recorded interactions in ${name}: used ${used} of ${interactions.length}`), + 43 4 : ) + 44 26 : return yield* Effect.void + 45 : }), + 46 4 : ) + 47 : + 48 12 : return { + 49 20 : claim: (validate) => + 50 38 : Effect.flatMap(load, (interactions) => + 51 49 : SynchronizedRef.modifyEffect(position, (index) => + 52 19 : Effect.gen(function* () { + 53 46 : const interaction = interactions[index] + 54 56 : yield* validate(interaction, index, interactions) + 55 37 : if (interaction === undefined) + 56 6 : return yield* Effect.die("Replay validation accepted a missing interaction") + 57 43 : return [{ interaction, index }, index + 1] as const + 58 : }), + 59 : ), + 60 2 : ), + 61 1 : } + 62 1 : }) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Schema } from "effect" + 2 : + 3 37 : export const REDACTED = "[REDACTED]" + 4 : + 5 33 : const DEFAULT_REDACT_HEADERS = [ + 6 18 : "authorization", + 7 11 : "cookie", + 8 24 : "proxy-authorization", + 9 15 : "set-cookie", + 10 14 : "x-api-key", + 11 25 : "x-amz-security-token", + 12 17 : "x-goog-api-key", + 13 2 : ] + 14 : + 15 31 : const DEFAULT_REDACT_QUERY = [ + 16 17 : "access_token", + 17 12 : "api-key", + 18 12 : "api_key", + 19 11 : "apikey", + 20 9 : "code", + 21 8 : "key", + 22 14 : "signature", + 23 8 : "sig", + 24 10 : "token", + 25 21 : "x-amz-credential", + 26 25 : "x-amz-security-token", + 27 18 : "x-amz-signature", + 28 2 : ] + 29 : + 30 26 : const SECRET_PATTERNS: ReadonlyArray<{ readonly label: string; readonly pattern: RegExp }> = [ + 31 78 : { label: "bearer token", pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}\b/i }, + 32 72 : { label: "API key", pattern: /\bsk-[A-Za-z0-9][A-Za-z0-9_-]{20,}\b/ }, + 33 75 : { label: "Anthropic API key", pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/ }, + 34 69 : { label: "Google API key", pattern: /\bAIza[0-9A-Za-z_-]{20,}\b/ }, + 35 72 : { label: "AWS access key", pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ }, + 36 72 : { label: "GitHub token", pattern: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/ }, + 37 72 : { label: "private key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ }, + 38 2 : ] + 39 : + 40 85 : const ENV_SECRET_NAMES = /(?:API|AUTH|BEARER|CREDENTIAL|KEY|PASSWORD|SECRET|TOKEN)/i + 41 65 : const SAFE_ENV_VALUES = new Set(["fixture", "test", "test-key"]) + 42 : + 43 0 : const envSecrets = () => + 44 0 : Object.entries(process.env).flatMap(([name, value]) => { + 45 0 : if (!value) return [] + 46 0 : if (!ENV_SECRET_NAMES.test(name)) return [] + 47 0 : if (value.length < 12) return [] + 48 0 : if (SAFE_ENV_VALUES.has(value.toLowerCase())) return [] + 49 : return [{ name, value }] + 50 2 : }) + 51 : + 52 16 : const pathFor = (base: string, key: string) => (base ? `${base}.${key}` : key) + 53 : + 54 0 : const stringEntries = (value: unknown, base = ""): ReadonlyArray<{ readonly path: string; readonly value: string }> => { + 55 0 : if (typeof value === "string") return [{ path: base, value }] + 56 0 : if (Array.isArray(value)) return value.flatMap((item, index) => stringEntries(item, `${base}[${index}]`)) + 57 0 : if (value && typeof value === "object") { + 58 0 : return Object.entries(value).flatMap(([key, child]) => stringEntries(child, pathFor(base, key))) + 59 0 : } + 60 2 : return [] + 61 : } + 62 : + 63 41 : const redactionSet = (values: ReadonlyArray<string> | undefined, defaults: ReadonlyArray<string>) => + 64 74 : new Set([...defaults, ...(values ?? [])].map((value) => value.toLowerCase())) + 65 : + 66 : export type UrlRedactor = (url: string) => string + 67 : + 68 24 : export const redactUrl = ( + 69 5 : raw: string, + 70 30 : query: ReadonlyArray<string> = DEFAULT_REDACT_QUERY, + 71 16 : urlRedactor?: UrlRedactor, + 72 3 : ) => { + 73 28 : if (!URL.canParse(raw)) return urlRedactor?.(raw) ?? raw + 74 27 : const url = new URL(raw) + 75 22 : if (url.username) url.username = REDACTED + 76 22 : if (url.password) url.password = REDACTED + 77 61 : const redacted = redactionSet(query, DEFAULT_REDACT_QUERY) + 78 42 : for (const key of url.searchParams.keys()) { + 79 0 : if (redacted.has(key.toLowerCase())) url.searchParams.set(key, REDACTED) + 80 2 : } + 81 56 : return urlRedactor?.(url.toString()) ?? url.toString() + 82 : } + 83 : + 84 28 : export const redactHeaders = ( + 85 9 : headers: Record<string, string>, + 86 7 : allow: ReadonlyArray<string>, + 87 36 : redact: ReadonlyArray<string> = DEFAULT_REDACT_HEADERS, + 88 3 : ) => { + 89 65 : const allowed = new Set(allow.map((name) => name.toLowerCase())) + 90 64 : const redacted = redactionSet(redact, DEFAULT_REDACT_HEADERS) + 91 26 : return Object.fromEntries( + 92 24 : Object.entries(headers) + 93 50 : .map(([name, value]) => [name.toLowerCase(), value] as const) + 94 36 : .filter(([name]) => allowed.has(name)) + 95 58 : .map(([name, value]) => [name, redacted.has(name) ? REDACTED : value] as const) + 96 9 : .toSorted(([a], [b]) => a.localeCompare(b)), + 97 3 : ) + 98 : } + 99 : + 100 51 : export const SecretFindingSchema = Schema.Struct({ + 101 22 : path: Schema.String, + 102 22 : reason: Schema.String, + 103 3 : }) + 104 : export type SecretFinding = Schema.Schema.Type<typeof SecretFindingSchema> + 105 : + 106 0 : export const secretFindings = (value: unknown): ReadonlyArray<SecretFinding> => { + 107 0 : const environment = envSecrets() + 108 0 : return stringEntries(value).flatMap((entry) => [ + 109 0 : ...SECRET_PATTERNS.filter((item) => item.pattern.test(entry.value)).map((item) => ({ + 110 0 : path: entry.path, + 111 0 : reason: item.label, + 112 0 : })), + 113 0 : ...environment + 114 0 : .filter((item) => entry.value.includes(item.value)) + 115 0 : .map((item) => ({ path: entry.path, reason: `environment secret ${item.name}` })), + 116 1 : ]) + 117 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Option } from "effect" + 2 43 : import { decodeJson } from "./matching.js" + 3 68 : import { REDACTED, redactHeaders, redactUrl } from "./redaction.js" + 4 : import type { RedactOptions, RequestSnapshot, ResponseSnapshot } from "./types.js" + 5 : + 6 : export type { RedactOptions } from "./types.js" + 7 : + 8 81 : export const DEFAULT_REQUEST_HEADERS: ReadonlyArray<string> = ["content-type", "accept", "openai-beta"] + 9 57 : export const DEFAULT_RESPONSE_HEADERS: ReadonlyArray<string> = ["content-type"] + 10 : + 11 17 : const identity = <T>(value: T) => value + 12 : + 13 : export interface Redactor { + 14 : readonly request: (snapshot: RequestSnapshot) => RequestSnapshot + 15 : readonly response: (snapshot: ResponseSnapshot) => ResponseSnapshot + 16 : } + 17 : + 18 42 : export const compose = (...redactors: ReadonlyArray<Partial<Redactor>>): Redactor => { + 19 80 : const requests = redactors.map((r) => r.request).filter((fn): fn is Redactor["request"] => fn !== undefined) + 20 82 : const responses = redactors.map((r) => r.response).filter((fn): fn is Redactor["response"] => fn !== undefined) + 21 12 : return { + 22 95 : request: requests.length === 0 ? identity : (snapshot) => requests.reduce((acc, fn) => fn(acc), snapshot), + 23 36 : response: responses.length === 0 ? identity : (snapshot) => responses.reduce((acc, fn) => fn(acc), snapshot), + 24 3 : } + 25 : } + 26 : + 27 : export interface HeaderOptions { + 28 : readonly allow?: ReadonlyArray<string> + 29 : readonly redact?: ReadonlyArray<string> + 30 : } + 31 : + 32 50 : export const requestHeaders = (options: HeaderOptions = {}): Partial<Redactor> => ({ + 33 31 : request: (snapshot) => ({ + 34 13 : ...snapshot, + 35 100 : headers: redactHeaders(snapshot.headers, options.allow ?? DEFAULT_REQUEST_HEADERS, options.redact), + 36 2 : }), + 37 2 : }) + 38 : + 39 51 : export const responseHeaders = (options: HeaderOptions = {}): Partial<Redactor> => ({ + 40 0 : response: (snapshot) => ({ + 41 0 : ...snapshot, + 42 0 : headers: redactHeaders(snapshot.headers, options.allow ?? DEFAULT_RESPONSE_HEADERS, options.redact), + 43 1 : }), + 44 2 : }) + 45 : + 46 : export interface UrlOptions { + 47 : readonly query?: ReadonlyArray<string> + 48 : readonly transform?: (url: string) => string + 49 : } + 50 : + 51 39 : export const url = (options: UrlOptions = {}): Partial<Redactor> => ({ + 52 103 : request: (snapshot) => ({ ...snapshot, url: redactUrl(snapshot.url, options.query, options.transform) }), + 53 2 : }) + 54 : + 55 0 : export const body = (transform: (parsed: unknown) => unknown): Partial<Redactor> => ({ + 56 0 : request: (snapshot) => ({ + 57 0 : ...snapshot, + 58 0 : body: Option.match(decodeJson(snapshot.body), { + 59 0 : onNone: () => snapshot.body, + 60 0 : onSome: (parsed) => JSON.stringify(transform(parsed)), + 61 0 : }), + 62 : }), + 63 2 : }) + 64 : + 65 : export interface DefaultRedactorOverrides { + 66 : readonly requestHeaders?: HeaderOptions + 67 : readonly responseHeaders?: HeaderOptions + 68 : readonly url?: UrlOptions + 69 : readonly body?: (parsed: unknown) => unknown + 70 : } + 71 : + 72 37 : const DEFAULT_REDACT_JSON_FIELDS = [ + 73 17 : "access_token", + 74 12 : "api_key", + 75 11 : "apikey", + 76 18 : "client_secret", + 77 13 : "password", + 78 18 : "refresh_token", + 79 11 : "secret", + 80 8 : "token", + 81 2 : ] + 82 : + 83 79 : const normalizeField = (field: string) => field.replace(/[^a-z0-9]/gi, "").toLowerCase() + 84 : + 85 45 : const redactJsonFields = (value: unknown, fields: ReadonlySet<string>): unknown => { + 86 86 : if (Array.isArray(value)) return value.map((item) => redactJsonFields(item, fields)) + 87 57 : if (!value || typeof value !== "object") return value + 88 26 : return Object.fromEntries( + 89 48 : Object.entries(value).map(([key, child]) => [ + 90 8 : key, + 91 67 : fields.has(normalizeField(key)) ? REDACTED : redactJsonFields(child, fields), + 92 1 : ]), + 93 3 : ) + 94 : } + 95 : + 96 50 : const redactBody = (value: string, fields: ReadonlySet<string>, transform: ((body: string) => string) | undefined) => { + 97 54 : const redacted = Option.match(decodeJson(value), { + 98 12 : onNone: () => value, + 99 68 : onSome: (parsed) => JSON.stringify(redactJsonFields(parsed, fields)), + 100 5 : }) + 101 42 : return transform?.(redacted) ?? redacted + 102 : } + 103 : + 104 39 : export const make = (options: RedactOptions = {}): Redactor => { + 105 107 : const fields = new Set([...DEFAULT_REDACT_JSON_FIELDS, ...(options.jsonFields ?? [])].map(normalizeField)) + 106 15 : return compose( + 107 20 : requestHeaders({ + 108 104 : allow: [...DEFAULT_REQUEST_HEADERS, ...(options.allowRequestHeaders ?? []), ...(options.headers ?? [])], + 109 25 : redact: options.headers, + 110 4 : }), + 111 21 : responseHeaders({ + 112 106 : allow: [...DEFAULT_RESPONSE_HEADERS, ...(options.allowResponseHeaders ?? []), ...(options.headers ?? [])], + 113 25 : redact: options.headers, + 114 4 : }), + 115 65 : url({ query: options.queryParameters, transform: options.url }), + 116 5 : { + 117 33 : request: (snapshot) => ({ + 118 15 : ...snapshot, + 119 57 : body: redactBody(snapshot.body, fields, options.body), + 120 6 : }), + 121 0 : response: (snapshot) => ({ + 122 0 : ...snapshot, + 123 0 : body: redactBody(snapshot.body, fields, options.body), + 124 2 : }), + 125 1 : }, + 126 3 : ) + 127 : } + 128 : + 129 0 : export const defaults = (overrides: DefaultRedactorOverrides = {}): Redactor => + 130 0 : compose( + 131 0 : requestHeaders(overrides.requestHeaders), + 132 0 : responseHeaders(overrides.responseHeaders), + 133 0 : url(overrides.url), + 134 : ...(overrides.body ? [body(overrides.body)] : []), + 135 1 : ) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Schema } from "effect" + 2 : import type { + 3 : CassetteMetadata, + 4 : HttpInteraction, + 5 : RequestSnapshot, + 6 : ResponseSnapshot, + 7 : WebSocketEvent, + 8 : WebSocketInteraction, + 9 : } from "./types.js" + 10 : + 11 : export type { + 12 : CassetteMetadata, + 13 : HttpInteraction, + 14 : RequestSnapshot, + 15 : ResponseSnapshot, + 16 : WebSocketEvent, + 17 : WebSocketInteraction, + 18 : } from "./types.js" + 19 : + 20 53 : export const RequestSnapshotSchema = Schema.Struct({ + 21 24 : method: Schema.String, + 22 21 : url: Schema.String, + 23 55 : headers: Schema.Record(Schema.String, Schema.String), + 24 20 : body: Schema.String, + 25 3 : }) + 26 : + 27 54 : export const ResponseSnapshotSchema = Schema.Struct({ + 28 24 : status: Schema.Number, + 29 55 : headers: Schema.Record(Schema.String, Schema.String), + 30 22 : body: Schema.String, + 31 67 : bodyEncoding: Schema.optional(Schema.Literals(["text", "base64"])), + 32 3 : }) + 33 : + 34 83 : export const CassetteMetadataSchema = Schema.Record(Schema.String, Schema.Unknown) + 35 : + 36 53 : export const HttpInteractionSchema = Schema.Struct({ + 37 32 : transport: Schema.tag("http"), + 38 33 : request: RequestSnapshotSchema, + 39 33 : response: ResponseSnapshotSchema, + 40 3 : }) + 41 : + 42 51 : export const WebSocketEventSchema = Schema.Union([ + 43 19 : Schema.Struct({ + 44 53 : direction: Schema.Literals(["client", "server"]), + 45 29 : kind: Schema.tag("text"), + 46 21 : body: Schema.String, + 47 5 : }), + 48 19 : Schema.Struct({ + 49 53 : direction: Schema.Literals(["client", "server"]), + 50 31 : kind: Schema.tag("binary"), + 51 24 : body: Schema.String, + 52 40 : bodyEncoding: Schema.Literal("base64"), + 53 3 : }), + 54 3 : ]) + 55 : + 56 58 : export const WebSocketInteractionSchema = Schema.Struct({ + 57 37 : transport: Schema.tag("websocket"), + 58 25 : open: Schema.Struct({ + 59 23 : url: Schema.String, + 60 54 : headers: Schema.Record(Schema.String, Schema.String), + 61 5 : }), + 62 43 : events: Schema.Array(WebSocketEventSchema), + 63 3 : }) + 64 : + 65 103 : export const InteractionSchema = Schema.Union([HttpInteractionSchema, WebSocketInteractionSchema]).pipe( + 66 33 : Schema.toTaggedUnion("transport"), + 67 3 : ) + 68 : export type Interaction = Schema.Schema.Type<typeof InteractionSchema> + 69 : + 70 63 : export const isHttpInteraction = InteractionSchema.guards.http + 71 : + 72 73 : export const isWebSocketInteraction = InteractionSchema.guards.websocket + 73 : + 74 87 : export const httpInteractions = (interactions: ReadonlyArray<Interaction>) => interactions.filter(isHttpInteraction) + 75 : + 76 0 : export const webSocketInteractions = (interactions: ReadonlyArray<Interaction>) => + 77 2 : interactions.filter(isWebSocketInteraction) + 78 : + 79 46 : export const CassetteSchema = Schema.Struct({ + 80 29 : version: Schema.Literal(1), + 81 52 : metadata: Schema.optional(CassetteMetadataSchema), + 82 46 : interactions: Schema.Array(InteractionSchema), + 83 3 : }) + 84 : export type Cassette = Schema.Schema.Type<typeof CassetteSchema> + 85 : + 86 71 : export const decodeCassette = Schema.decodeUnknownSync(CassetteSchema) + 87 63 : export const encodeCassette = Schema.encodeSync(CassetteSchema) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 55 : import { NodeFileSystem } from "@effect/platform-node" + 2 81 : import { Deferred, Effect, Exit, FiberSet, Layer, Ref, Scope, Semaphore } from "effect" + 3 48 : import { Socket } from "effect/unstable/socket" + 4 49 : import * as CassetteService from "./cassette.js" + 5 71 : import { canonicalizeJson, decodeJson, safeText } from "./matching.js" + 6 65 : import { makeReplayState, resolveAutoMode } from "./recorder.js" + 7 37 : import { make, type Redactor } from "./redactor.js" + 8 52 : import { webSocketInteractions } from "./schema.js" + 9 : import type { + 10 : RecorderOptions, + 11 : WebSocketEvent, + 12 : WebSocketInteraction, + 13 : WebSocketRecorderOptions, + 14 : WebSocketRequest, + 15 : } from "./types.js" + 16 : + 17 : interface ActiveReplay { + 18 : readonly interaction: WebSocketInteraction + 19 : readonly progress: Ref.Ref<{ readonly position: number; readonly changed: Deferred.Deferred<void> }> + 20 : readonly writeLock: Semaphore.Semaphore + 21 : readonly closed: Ref.Ref<boolean> + 22 : } + 23 : + 24 : interface ActiveRecording { + 25 : readonly events: Array<WebSocketEvent> + 26 : readonly eventLock: Semaphore.Semaphore + 27 : readonly accepting: Ref.Ref<boolean> + 28 : opened: boolean + 29 : valid: boolean + 30 : } + 31 : + 32 : type Frame = string | Uint8Array + 33 : + 34 0 : const encodeEvent = (direction: "client" | "server", message: Frame): WebSocketEvent => + 35 0 : typeof message === "string" + 36 0 : ? { direction, kind: "text", body: message } + 37 2 : : { direction, kind: "binary", body: Buffer.from(message).toString("base64"), bodyEncoding: "base64" } + 38 : + 39 0 : const decodeEvent = (event: WebSocketEvent): Frame => + 40 2 : event.kind === "text" ? event.body : new Uint8Array(Buffer.from(event.body, "base64")) + 41 : + 42 0 : const redactEvent = (event: WebSocketEvent, redactor: Redactor): WebSocketEvent => { + 43 0 : if (event.kind === "binary") return event + 44 0 : const body = + 45 0 : event.direction === "client" + 46 0 : ? redactor.request({ method: "WEBSOCKET", url: "", headers: {}, body: event.body }).body + 47 0 : : redactor.response({ status: 101, headers: {}, body: event.body }).body + 48 2 : return { ...event, body } + 49 : } + 50 : + 51 0 : const comparable = (event: WebSocketEvent, asJson: boolean) => { + 52 0 : if (!asJson || event.kind === "binary") return JSON.stringify(canonicalizeJson(event)) + 53 0 : const decoded = decodeJson(event.body) + 54 0 : return JSON.stringify( + 55 0 : canonicalizeJson({ + 56 0 : ...event, + 57 0 : body: decoded._tag === "None" ? event.body : canonicalizeJson(decoded.value), + 58 0 : }), + 59 2 : ) + 60 : } + 61 : + 62 0 : const assertEvent = (actual: WebSocketEvent, expected: WebSocketEvent | undefined, index: number, asJson: boolean) => + 63 0 : Effect.sync(() => { + 64 0 : if (expected && comparable(actual, asJson) === comparable(expected, asJson)) return + 65 : throw new Error(`WebSocket event ${index + 1}: expected ${safeText(expected)}, received ${safeText(actual)}`) + 66 2 : }) + 67 : + 68 0 : const runHandler = <A, E, R>(handler: (value: A) => Effect.Effect<unknown, E, R> | void, value: A) => + 69 0 : Effect.suspend(() => { + 70 0 : const result = handler(value) + 71 : return Effect.isEffect(result) ? Effect.asVoid(result) : Effect.void + 72 2 : }) + 73 : + 74 0 : const runReplay = <A, E, R>( + 75 0 : state: ActiveReplay, + 76 0 : handler: (value: A) => Effect.Effect<unknown, E, R> | void, + 77 0 : decode: (event: WebSocketEvent) => A, + 78 0 : onOpen: Effect.Effect<void> | undefined, + 79 0 : ) => + 80 0 : Effect.scoped( + 81 0 : Effect.gen(function* () { + 82 0 : const handlers = yield* FiberSet.make<unknown, E>() + 83 0 : const run = yield* FiberSet.runtime(handlers)<R>() + 84 0 : if (onOpen) yield* onOpen + 85 0 : + 86 0 : const drive = Effect.gen(function* () { + 87 0 : while (true) { + 88 0 : const current = yield* Ref.get(state.progress) + 89 0 : const event = state.interaction.events[current.position] + 90 0 : if (!event) return + 91 0 : if (yield* Ref.get(state.closed)) + 92 0 : return yield* Effect.die( + 93 0 : new Error( + 94 0 : `WebSocket closed with unconsumed events: used ${current.position} of ${state.interaction.events.length}`, + 95 0 : ), + 96 0 : ) + 97 0 : if (event.direction === "server") { + 98 0 : yield* Ref.set(state.progress, { + 99 0 : position: current.position + 1, + 100 0 : changed: yield* Deferred.make<void>(), + 101 0 : }) + 102 0 : run(runHandler(handler, decode(event))) + 103 0 : continue + 104 0 : } + 105 0 : yield* Deferred.await(current.changed) + 106 0 : } + 107 0 : }) + 108 0 : + 109 0 : yield* drive.pipe(Effect.raceFirst(FiberSet.join(handlers))) + 110 0 : yield* FiberSet.awaitEmpty(handlers).pipe(Effect.raceFirst(FiberSet.join(handlers))) + 111 : }), + 112 2 : ) + 113 : + 114 0 : const openSnapshot = (request: WebSocketRequest, redactor: Redactor) => { + 115 0 : const snapshot = redactor.request({ method: "GET", url: request.url, headers: request.headers ?? {}, body: "" }) + 116 2 : return { url: snapshot.url, headers: snapshot.headers } + 117 : } + 118 : + 119 0 : const makeRecordingSocket = ( + 120 0 : upstream: Socket.Socket, + 121 0 : cassette: CassetteService.Interface, + 122 0 : name: string, + 123 0 : request: WebSocketRequest, + 124 0 : options: WebSocketRecorderOptions, + 125 0 : redactor: Redactor, + 126 0 : ) => + 127 0 : Effect.gen(function* () { + 128 0 : const active = yield* Ref.make<ActiveRecording | undefined>(undefined) + 129 0 : const writeLock = yield* Semaphore.make(1) + 130 0 : + 131 0 : return Socket.make({ + 132 0 : runRaw: (handler, runOptions) => + 133 0 : Effect.gen(function* () { + 134 0 : const state: ActiveRecording = { + 135 0 : events: [], + 136 0 : eventLock: yield* Semaphore.make(1), + 137 0 : accepting: yield* Ref.make(true), + 138 0 : opened: false, + 139 0 : valid: true, + 140 0 : } + 141 0 : const occupied = yield* Ref.modify(active, (current) => [current !== undefined, current ?? state]) + 142 0 : if (occupied) return yield* Effect.die("Concurrent runs of a recorded WebSocket are not supported") + 143 0 : yield* upstream + 144 0 : .runRaw( + 145 0 : (message) => { + 146 0 : if (!Ref.getUnsafe(state.accepting)) throw new Error("WebSocket received a frame after closing") + 147 0 : state.events.push(redactEvent(encodeEvent("server", message), redactor)) + 148 0 : return handler(message) + 149 0 : }, + 150 0 : { + 151 0 : ...runOptions, + 152 0 : onOpen: Effect.gen(function* () { + 153 0 : state.opened = true + 154 0 : if (runOptions?.onOpen) yield* runOptions.onOpen + 155 0 : }), + 156 0 : }, + 157 0 : ) + 158 0 : .pipe( + 159 0 : Effect.onExit((exit) => + 160 0 : writeLock.withPermit( + 161 0 : state.eventLock.withPermit( + 162 0 : Effect.gen(function* () { + 163 0 : yield* Ref.set(state.accepting, false) + 164 0 : yield* Ref.set(active, undefined) + 165 0 : if (!Exit.isSuccess(exit) || !state.opened || !state.valid) return + 166 0 : yield* cassette + 167 0 : .append( + 168 0 : name, + 169 0 : { + 170 0 : transport: "websocket", + 171 0 : open: openSnapshot(request, redactor), + 172 0 : events: [...state.events], + 173 0 : }, + 174 0 : options.metadata, + 175 0 : ) + 176 0 : .pipe(Effect.orDie) + 177 0 : }), + 178 0 : ), + 179 0 : ), + 180 0 : ), + 181 0 : ) + 182 0 : }), + 183 0 : writer: upstream.writer.pipe( + 184 0 : Effect.map( + 185 0 : (write) => (message) => + 186 0 : writeLock.withPermit( + 187 0 : Effect.gen(function* () { + 188 0 : if (Socket.isCloseEvent(message)) return yield* write(message) + 189 0 : const state = yield* Ref.get(active) + 190 0 : if (!state || !(yield* Ref.get(state.accepting))) + 191 0 : return yield* Effect.die("WebSocket writer used without an active socket run") + 192 0 : const event = redactEvent(encodeEvent("client", message), redactor) + 193 0 : yield* state.eventLock.withPermit(Effect.sync(() => state.events.push(event))) + 194 0 : return yield* write(message).pipe(Effect.onError(() => Effect.sync(() => (state.valid = false)))) + 195 0 : }), + 196 0 : ), + 197 0 : ), + 198 0 : ), + 199 : }) + 200 2 : }) + 201 : + 202 0 : const makeReplaySocket = ( + 203 0 : cassette: CassetteService.Interface, + 204 0 : name: string, + 205 0 : request: WebSocketRequest, + 206 0 : options: WebSocketRecorderOptions, + 207 0 : redactor: Redactor, + 208 0 : ): Effect.Effect<Socket.Socket, never, Scope.Scope> => + 209 0 : Effect.gen(function* () { + 210 0 : const replay = yield* makeReplayState(cassette, name, webSocketInteractions) + 211 0 : const active = yield* Ref.make<ActiveReplay | undefined>(undefined) + 212 0 : + 213 0 : return Socket.make({ + 214 0 : runRaw: (handler, runOptions) => + 215 0 : Effect.gen(function* () { + 216 0 : const claimed = yield* replay + 217 0 : .claim((interaction, index) => + 218 0 : Effect.sync(() => { + 219 0 : const incoming = openSnapshot(request, redactor) + 220 0 : if ( + 221 0 : interaction && + 222 0 : JSON.stringify(canonicalizeJson(incoming)) === JSON.stringify(canonicalizeJson(interaction.open)) + 223 0 : ) + 224 0 : return + 225 0 : throw new Error( + 226 0 : `WebSocket open ${index + 1}: expected ${safeText(interaction?.open)}, received ${safeText(incoming)}`, + 227 0 : ) + 228 0 : }), + 229 0 : ) + 230 0 : .pipe(Effect.orDie) + 231 0 : const progress = yield* Ref.make({ position: 0, changed: yield* Deferred.make<void>() }) + 232 0 : const writeLock = yield* Semaphore.make(1) + 233 0 : const state = { + 234 0 : interaction: claimed.interaction, + 235 0 : progress, + 236 0 : writeLock, + 237 0 : closed: yield* Ref.make(false), + 238 0 : } + 239 0 : const occupied = yield* Ref.modify(active, (current) => [current !== undefined, current ?? state]) + 240 0 : if (occupied) return yield* Effect.die("Concurrent runs of a replayed WebSocket are not supported") + 241 0 : yield* runReplay(state, handler, decodeEvent, runOptions?.onOpen).pipe( + 242 0 : Effect.ensuring(Ref.set(active, undefined)), + 243 0 : ) + 244 0 : }), + 245 0 : writer: Effect.succeed((message) => { + 246 0 : return Ref.get(active).pipe( + 247 0 : Effect.flatMap((state) => + 248 0 : state + 249 0 : ? state.writeLock.withPermit( + 250 0 : Effect.gen(function* () { + 251 0 : const current = yield* Ref.get(state.progress) + 252 0 : if (Socket.isCloseEvent(message)) { + 253 0 : yield* Ref.set(state.closed, true) + 254 0 : yield* Deferred.succeed(current.changed, undefined) + 255 0 : if (current.position === state.interaction.events.length) return + 256 0 : return yield* Effect.die( + 257 0 : new Error( + 258 0 : `WebSocket closed with unconsumed events: used ${current.position} of ${state.interaction.events.length}`, + 259 0 : ), + 260 0 : ) + 261 0 : } + 262 0 : const actual = redactEvent(encodeEvent("client", message), redactor) + 263 0 : yield* assertEvent( + 264 0 : actual, + 265 0 : state.interaction.events[current.position], + 266 0 : current.position, + 267 0 : options.compareClientMessagesAsJson === true, + 268 0 : ) + 269 0 : yield* Ref.set(state.progress, { + 270 0 : position: current.position + 1, + 271 0 : changed: yield* Deferred.make<void>(), + 272 0 : }) + 273 0 : yield* Deferred.succeed(current.changed, undefined) + 274 0 : }), + 275 0 : ) + 276 0 : : Effect.die("WebSocket writer used without an active socket run"), + 277 0 : ), + 278 0 : ) + 279 0 : }), + 280 : }) + 281 2 : }) + 282 : + 283 0 : const recordingLayer = ( + 284 0 : name: string, + 285 0 : request: WebSocketRequest, + 286 0 : options: WebSocketRecorderOptions, + 287 0 : forcedMode?: "record" | "replay", + 288 0 : ): Layer.Layer<Socket.Socket, never, Socket.Socket | CassetteService.Service> => + 289 0 : Layer.effect( + 290 0 : Socket.Socket, + 291 0 : Effect.gen(function* () { + 292 0 : const upstream = yield* Socket.Socket + 293 0 : const cassette = yield* CassetteService.Service + 294 0 : const redactor = make(options.redact) + 295 0 : if ((forcedMode ?? (yield* resolveAutoMode(cassette, name))) === "record") + 296 0 : return yield* makeRecordingSocket(upstream, cassette, name, request, options, redactor) + 297 0 : return yield* makeReplaySocket(cassette, name, request, options, redactor) + 298 : }), + 299 2 : ) + 300 : + 301 : /** + 302 : * Wraps a provided `Socket.Socket` with cassette recording and replay. + 303 : * + 304 : * Supply the ordinary URL-bound Effect socket layer beneath this decorator. + 305 : * The cassette name identifies the connection; recorder configuration does not + 306 : * duplicate the transport URL. + 307 : */ + 308 0 : export const socket = (name: string, options: RecorderOptions = {}): Layer.Layer<Socket.Socket, never, Socket.Socket> => + 309 2 : provideCassette(recordingLayer(name, { url: "" }, { ...options, compareClientMessagesAsJson: true }), options) + 310 : + 311 : /** @internal */ + 312 0 : export const socketLayer = ( + 313 0 : name: string, + 314 0 : request: WebSocketRequest, + 315 0 : options: WebSocketRecorderOptions & { readonly mode: "record" | "replay" }, + 316 0 : ): Layer.Layer<Socket.Socket, never, Socket.Socket> => + 317 2 : provideCassette(recordingLayer(name, request, options, options.mode), options) + 318 : + 319 0 : const provideCassette = ( + 320 0 : layer: Layer.Layer<Socket.Socket, never, Socket.Socket | CassetteService.Service>, + 321 0 : options: WebSocketRecorderOptions, + 322 0 : ) => + 323 0 : layer.pipe( + 324 0 : Layer.provide(CassetteService.fileSystem({ directory: options.directory })), + 325 : Layer.provide(NodeFileSystem.layer), + 326 1 : ) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 81 : import { Effect, Option, Ref, Scope, Semaphore, Stream, SynchronizedRef } from "effect" + 2 : import type { Headers } from "effect/unstable/http" + 3 : import * as CassetteService from "./cassette.js" + 4 71 : import { canonicalizeJson, decodeJson, safeText } from "./matching.js" + 5 65 : import { makeReplayState, resolveAutoMode } from "./recorder.js" + 6 : import type { RecordReplayMode } from "./internal-effect.js" + 7 37 : import { make, type Redactor } from "./redactor.js" + 8 52 : import { webSocketInteractions, type CassetteMetadata, type WebSocketEvent } from "./schema.js" + 9 : + 10 : export interface WebSocketRequest { + 11 : readonly url: string + 12 : readonly headers: Headers.Headers + 13 : } + 14 : + 15 : export interface WebSocketConnection<E> { + 16 : readonly sendText: (message: string) => Effect.Effect<void, E> + 17 : readonly messages: Stream.Stream<string | Uint8Array, E> + 18 : readonly close: Effect.Effect<void> + 19 : } + 20 : + 21 : export interface WebSocketExecutor<E> { + 22 : readonly open: (request: WebSocketRequest) => Effect.Effect<WebSocketConnection<E>, E> + 23 : } + 24 : + 25 : export interface WebSocketRecordReplayOptions<E> { + 26 : readonly name: string + 27 : readonly mode?: RecordReplayMode + 28 : readonly metadata?: CassetteMetadata + 29 : readonly cassette: CassetteService.Interface + 30 : readonly live: WebSocketExecutor<E> + 31 : readonly redactor?: Redactor + 32 : readonly compareClientMessagesAsJson?: boolean + 33 : } + 34 : + 35 0 : const headersRecord = (headers: Headers.Headers): Record<string, string> => + 36 0 : Object.fromEntries( + 37 0 : Object.entries(headers as Record<string, unknown>).filter( + 38 0 : (entry): entry is [string, string] => typeof entry[1] === "string", + 39 : ), + 40 2 : ) + 41 : + 42 0 : const textEvent = (direction: "client" | "server", body: string): WebSocketEvent => ({ + 43 0 : direction, + 44 0 : kind: "text", + 45 : body, + 46 2 : }) + 47 : + 48 0 : const decodeEvent = (event: WebSocketEvent) => + 49 2 : event.kind === "text" ? event.body : new Uint8Array(Buffer.from(event.body, "base64")) + 50 : + 51 19 : const jsonOrText = (value: string) => Option.match(decodeJson(value), { onNone: () => value, onSome: canonicalizeJson }) + 52 : + 53 0 : const assertClientEvent = (actual: string, expected: WebSocketEvent | undefined, index: number, asJson: boolean) => + 54 0 : Effect.sync(() => { + 55 0 : const matches = + 56 0 : expected?.direction === "client" && + 57 0 : expected.kind === "text" && + 58 0 : JSON.stringify(asJson ? jsonOrText(actual) : actual) === + 59 0 : JSON.stringify(asJson ? jsonOrText(expected.body) : expected.body) + 60 0 : if (matches) return + 61 : throw new Error(`WebSocket client frame ${index + 1}: expected ${safeText(expected)}, received ${safeText(actual)}`) + 62 2 : }) + 63 : + 64 0 : export const makeWebSocketExecutor = <E>( + 65 0 : options: WebSocketRecordReplayOptions<E>, + 66 0 : ): Effect.Effect<WebSocketExecutor<E>, never, Scope.Scope> => + 67 0 : Effect.gen(function* () { + 68 0 : const mode = options.mode ?? (yield* resolveAutoMode(options.cassette, options.name)) + 69 0 : const redactor = options.redactor ?? make() + 70 0 : const openSnapshot = (request: WebSocketRequest) => { + 71 0 : const snapshot = redactor.request({ + 72 0 : method: "GET", + 73 0 : url: request.url, + 74 0 : headers: headersRecord(request.headers), + 75 0 : body: "", + 76 0 : }) + 77 0 : return { url: snapshot.url, headers: snapshot.headers } + 78 0 : } + 79 0 : const redactEvent = (event: WebSocketEvent) => { + 80 0 : if (event.kind === "binary") return event + 81 0 : const body = + 82 0 : event.direction === "client" + 83 0 : ? redactor.request({ method: "WEBSOCKET", url: "", headers: {}, body: event.body }).body + 84 0 : : redactor.response({ status: 101, headers: {}, body: event.body }).body + 85 0 : return { ...event, body } + 86 0 : } + 87 0 : + 88 0 : if (mode === "passthrough") return options.live + 89 0 : + 90 0 : if (mode === "record") { + 91 0 : return { + 92 0 : open: (request) => + 93 0 : Effect.gen(function* () { + 94 0 : const events: WebSocketEvent[] = [] + 95 0 : const connection = yield* options.live.open(request) + 96 0 : const closed = yield* Ref.make(false) + 97 0 : const closeLock = yield* Semaphore.make(1) + 98 0 : return { + 99 0 : sendText: (message) => + 100 0 : Effect.sync(() => events.push(redactEvent(textEvent("client", message)))).pipe( + 101 0 : Effect.andThen(connection.sendText(message)), + 102 0 : ), + 103 0 : messages: connection.messages.pipe( + 104 0 : Stream.tap((message) => + 105 0 : Effect.sync(() => + 106 0 : events.push( + 107 0 : typeof message === "string" + 108 0 : ? redactEvent(textEvent("server", message)) + 109 0 : : { + 110 0 : direction: "server", + 111 0 : kind: "binary", + 112 0 : body: Buffer.from(message).toString("base64"), + 113 0 : bodyEncoding: "base64", + 114 0 : }, + 115 0 : ), + 116 0 : ), + 117 0 : ), + 118 0 : ), + 119 0 : close: closeLock.withPermit( + 120 0 : Effect.gen(function* () { + 121 0 : if (yield* Ref.get(closed)) return + 122 0 : yield* connection.close + 123 0 : yield* options.cassette + 124 0 : .append( + 125 0 : options.name, + 126 0 : { transport: "websocket", open: openSnapshot(request), events }, + 127 0 : options.metadata, + 128 0 : ) + 129 0 : .pipe(Effect.orDie) + 130 0 : yield* Ref.set(closed, true) + 131 0 : }), + 132 0 : ), + 133 0 : } + 134 0 : }), + 135 0 : } + 136 0 : } + 137 0 : + 138 0 : const replay = yield* makeReplayState(options.cassette, options.name, webSocketInteractions) + 139 0 : return { + 140 0 : open: (request) => + 141 0 : Effect.gen(function* () { + 142 0 : const claimed = yield* replay + 143 0 : .claim((interaction, index) => + 144 0 : Effect.sync(() => { + 145 0 : const incoming = canonicalizeJson(openSnapshot(request)) + 146 0 : if (interaction && JSON.stringify(incoming) === JSON.stringify(canonicalizeJson(interaction.open))) + 147 0 : return + 148 0 : throw new Error(`WebSocket open ${index + 1} does not match ${safeText(incoming)}`) + 149 0 : }), + 150 0 : ) + 151 0 : .pipe(Effect.orDie) + 152 0 : const client = claimed.interaction.events.filter((event) => event.direction === "client") + 153 0 : const server = claimed.interaction.events.filter((event) => event.direction === "server") + 154 0 : const position = yield* SynchronizedRef.make(0) + 155 0 : return { + 156 0 : sendText: (message) => + 157 0 : SynchronizedRef.updateEffect(position, (index) => + 158 0 : assertClientEvent(message, client[index], index, options.compareClientMessagesAsJson === true).pipe( + 159 0 : Effect.as(index + 1), + 160 0 : ), + 161 0 : ), + 162 0 : messages: Stream.fromIterable(server).pipe(Stream.map(decodeEvent)), + 163 0 : close: Effect.gen(function* () { + 164 0 : const used = yield* SynchronizedRef.get(position) + 165 0 : if (used !== client.length) + 166 0 : return yield* Effect.die( + 167 0 : new Error(`WebSocket client frame count: expected ${client.length}, received ${used}`), + 168 0 : ) + 169 0 : }), + 170 0 : } + 171 0 : }), + 172 : } + 173 1 : }) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : // Apply an `LLMRequest.cache` policy by injecting `CacheHint`s onto the parts + 2 : // the policy designates. Runs once at compile time, before the per-protocol + 3 : // body builder, so the existing inline-hint lowering path handles the rest. + 4 : // + 5 : // The default `"auto"` shape places one breakpoint at the last tool definition, + 6 : // one at the last system part, and one at the latest user message. This + 7 : // matches what production agent harnesses (LangChain's caching middleware, + 8 : // kern-ai's 10x cost-reduction playbook) converge on for tool-use loops: the + 9 : // latest user message stays put while a single turn explodes into many + 10 : // assistant/tool round-trips, so caching at that boundary lets every + 11 : // intra-turn API call hit the prefix. + 12 : // + 13 : // Manual `cache: CacheHint` placements on individual parts are preserved — + 14 : // this function only fills gaps the caller left empty. + 15 45 : import { CacheHint, type CachePolicy, type CachePolicyObject } from "./schema/options" + 16 72 : import { LLMRequest, Message, ToolDefinition, type ContentPart } from "./schema/messages" + 17 : + 18 15 : const AUTO: CachePolicyObject = { + 19 14 : tools: true, + 20 15 : system: true, + 21 32 : messages: "latest-user-message", + 22 2 : } + 23 : + 24 16 : const NONE: CachePolicyObject = {} + 25 : + 26 : // Resolution rules: + 27 : // - undefined → "auto" — caching is on by default. The math favors it: + 28 : // Anthropic 5m-cache write is 1.25x base, read is 0.1x, + 29 : // so a single reuse within 5 minutes already wins. + 30 : // - "auto" → tools + system + latest user msg. + 31 : // - "none" → no auto placement; manual `CacheHint`s still flow. + 32 : // - object form → exactly what the caller asked for. + 33 0 : const resolve = (policy: CachePolicy | undefined): CachePolicyObject => { + 34 0 : if (policy === undefined || policy === "auto") return AUTO + 35 0 : if (policy === "none") return NONE + 36 2 : return policy + 37 : } + 38 : + 39 : // Protocols whose wire format ignores inline cache markers (OpenAI's implicit + 40 : // prefix caching, Gemini's implicit + out-of-band CachedContent). Skip the + 41 : // whole policy pass for these — emitting hints would be harmless but pointless. + 42 82 : const RESPECTS_INLINE_HINTS = new Set(["anthropic-messages", "bedrock-converse"]) + 43 : + 44 0 : const makeHint = (ttlSeconds: number | undefined): CacheHint => + 45 2 : ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" }) + 46 : + 47 0 : const markLastTool = (tools: ReadonlyArray<ToolDefinition>, hint: CacheHint): ReadonlyArray<ToolDefinition> => { + 48 0 : if (tools.length === 0) return tools + 49 0 : const last = tools.length - 1 + 50 0 : if (tools[last]!.cache) return tools + 51 2 : return tools.map((tool, i) => (i === last ? new ToolDefinition({ ...tool, cache: hint }) : tool)) + 52 : } + 53 : + 54 0 : const markLastSystem = (system: LLMRequest["system"], hint: CacheHint): LLMRequest["system"] => { + 55 0 : if (system.length === 0) return system + 56 0 : const last = system.length - 1 + 57 0 : if (system[last]!.cache) return system + 58 2 : return system.map((part, i) => (i === last ? { ...part, cache: hint } : part)) + 59 : } + 60 : + 61 0 : const lastIndexOfRole = (messages: ReadonlyArray<Message>, role: Message["role"]): number => + 62 2 : messages.findLastIndex((m) => m.role === role) + 63 : + 64 : // Mark the last text part of `messages[index]`. If no text part exists, mark + 65 : // the last content part regardless of type — that's the breakpoint position + 66 : // in tool-result-only messages too. + 67 0 : const markMessageAt = (messages: ReadonlyArray<Message>, index: number, hint: CacheHint): ReadonlyArray<Message> => { + 68 0 : if (index < 0 || index >= messages.length) return messages + 69 0 : const target = messages[index]! + 70 0 : if (target.content.length === 0) return messages + 71 0 : const lastTextIndex = target.content.findLastIndex((part) => part.type === "text") + 72 0 : const markAt = lastTextIndex >= 0 ? lastTextIndex : target.content.length - 1 + 73 0 : const existing = target.content[markAt]! + 74 0 : if ("cache" in existing && existing.cache) return messages + 75 0 : const nextContent = target.content.map((part, i) => (i === markAt ? ({ ...part, cache: hint } as ContentPart) : part)) + 76 0 : const next = new Message({ ...target, content: nextContent }) + 77 0 : // Single pass over `messages`, substituting the one updated entry. Long + 78 0 : // conversations call this on every request, so avoid `.map()` here — its + 79 0 : // closure dispatch and identity copies show up in profiling. + 80 0 : const result = messages.slice() + 81 0 : result[index] = next + 82 2 : return result + 83 : } + 84 : + 85 0 : const markMessages = ( + 86 0 : messages: ReadonlyArray<Message>, + 87 0 : strategy: NonNullable<CachePolicyObject["messages"]>, + 88 0 : hint: CacheHint, + 89 0 : ): ReadonlyArray<Message> => { + 90 0 : if (messages.length === 0) return messages + 91 0 : if (strategy === "latest-user-message") return markMessageAt(messages, lastIndexOfRole(messages, "user"), hint) + 92 0 : if (strategy === "latest-assistant") return markMessageAt(messages, lastIndexOfRole(messages, "assistant"), hint) + 93 0 : const start = Math.max(0, messages.length - strategy.tail) + 94 0 : let next = messages + 95 0 : for (let i = start; i < messages.length; i++) next = markMessageAt(next, i, hint) + 96 2 : return next + 97 : } + 98 : + 99 46 : export const applyCachePolicy = (request: LLMRequest): LLMRequest => { + 100 72 : if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request + 101 0 : const policy = resolve(request.cache) + 102 0 : if (!policy.tools && !policy.system && !policy.messages) return request + 103 : + 104 0 : const hint = makeHint(policy.ttlSeconds) + 105 0 : const tools = policy.tools ? markLastTool(request.tools, hint) : request.tools + 106 0 : const system = policy.system ? markLastSystem(request.system, hint) : request.system + 107 0 : const messages = policy.messages ? markMessages(request.messages, policy.messages, hint) : request.messages + 108 : + 109 0 : if (tools === request.tools && system === request.system && messages === request.messages) return request + 110 1 : return LLMRequest.update(request, { tools, system, messages }) + 111 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| cache-policy.ts | +
+ |
+ 29.2 % | +65 | +19 | +|
| index.ts | +
+ |
+ 100.0 % | +8 | +8 | +|
| llm.ts | +
+ |
+ 36.5 % | +104 | +38 | +|
| provider-error.ts | +
+ |
+ 94.9 % | +39 | +37 | +|
| provider.ts | +
+ |
+ 50.0 % | +4 | +2 | +|
| tool-runtime.ts | +
+ |
+ 11.5 % | +52 | +6 | +|
| tool.ts | +
+ |
+ 11.6 % | +69 | +8 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| tool-runtime.ts | +
+ |
+ 11.5 % | +52 | +6 | +|
| tool.ts | +
+ |
+ 11.6 % | +69 | +8 | +|
| cache-policy.ts | +
+ |
+ 29.2 % | +65 | +19 | +|
| llm.ts | +
+ |
+ 36.5 % | +104 | +38 | +|
| provider.ts | +
+ |
+ 50.0 % | +4 | +2 | +|
| provider-error.ts | +
+ |
+ 94.9 % | +39 | +37 | +|
| index.ts | +
+ |
+ 100.0 % | +8 | +8 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| cache-policy.ts | +
+ |
+ 29.2 % | +65 | +19 | +|
| index.ts | +
+ |
+ 100.0 % | +8 | +8 | +|
| llm.ts | +
+ |
+ 36.5 % | +104 | +38 | +|
| provider-error.ts | +
+ |
+ 94.9 % | +39 | +37 | +|
| provider.ts | +
+ |
+ 50.0 % | +4 | +2 | +|
| tool-runtime.ts | +
+ |
+ 11.5 % | +52 | +6 | +|
| tool.ts | +
+ |
+ 11.6 % | +69 | +8 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 43 : export { LLMClient } from "./route/client" + 2 36 : export { Auth } from "./route/auth" + 3 38 : export { Provider } from "./provider" + 4 79 : export { isContextOverflow, isContextOverflowFailure } from "./provider-error" + 5 : export type { + 6 : RouteModelInput, + 7 : RouteRoutedModelInput, + 8 : Interface as LLMClientShape, + 9 : Service as LLMClientService, + 10 : } from "./route/client" + 11 25 : export * from "./schema" + 12 58 : export { Tool, ToolFailure, toDefinitions } from "./tool" + 13 45 : export { ToolRuntime } from "./tool-runtime" + 14 : export type { DispatchResult as ToolDispatchResult, ToolSettlement } from "./tool-runtime" + 15 : export type { + 16 : AnyExecutableTool, + 17 : AnyTool, + 18 : ExecutableTool, + 19 : ExecutableTools, + 20 : Tool as ToolShape, + 21 : ToolExecute, + 22 : ToolExecuteContext, + 23 : ToolModelOutputInput, + 24 : Tools, + 25 : ToolSchema, + 26 : ToolToModelOutput, + 27 : } from "./tool" + 28 28 : export * as LLM from "./llm" + 29 : export type { + 30 : Definition as ProviderDefinition, + 31 : ModelFactory as ProviderModelFactory, + 32 : ModelOptions as ProviderModelOptions, + 33 : } from "./provider" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 80 : import { Effect, JsonSchema, Schema } from "effect" + 2 86 : import { LLMClient } from "./route/client" + 3 354 : import { + 4 : GenerationOptions, + 5 : HttpOptions, + 6 : InvalidProviderOutputReason, + 7 : LLMError, + 8 : LLMEvent, + 9 : LLMRequest, + 10 : LLMResponse, + 11 : Message, + 12 : type ModelInput as SchemaModelInput, + 13 : SystemPart, + 14 : ToolChoice, + 15 : ToolDefinition, + 16 : type ContentPart, + 17 : ToolResultPart, + 18 : } from "./schema" + 19 114 : import { make as makeTool, toDefinitions, type ToolSchema } from "./tool" + 20 : + 21 : export type ModelInput = SchemaModelInput + 22 : + 23 : export type MessageInput = Message.Input + 24 : + 25 : export type ToolChoiceInput = ToolChoice.Input + 26 : export type ToolChoiceMode = ToolChoice.Mode + 27 : + 28 : export type ToolResultInput = Parameters<typeof ToolResultPart.make>[0] + 29 : + 30 : /** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */ + 31 : export type RequestInput = Omit< + 32 : ConstructorParameters<typeof LLMRequest>[0], + 33 : "system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions" + 34 : > & { + 35 : readonly system?: string | SystemPart | ReadonlyArray<SystemPart> + 36 : readonly prompt?: string | ContentPart | ReadonlyArray<ContentPart> + 37 : readonly messages?: ReadonlyArray<Message | MessageInput> + 38 : readonly tools?: ReadonlyArray<ToolDefinition.Input> + 39 : readonly toolChoice?: ToolChoiceInput + 40 : readonly generation?: GenerationOptions.Input + 41 : readonly providerOptions?: ConstructorParameters<typeof LLMRequest>[0]["providerOptions"] + 42 : readonly http?: HttpOptions.Input + 43 : } + 44 : + 45 86 : export const generate = LLMClient.generate + 46 : + 47 78 : export const stream = LLMClient.stream + 48 : + 49 0 : export const requestInput = (input: LLMRequest): RequestInput => ({ + 50 24 : ...LLMRequest.input(input), + 51 5 : }) + 52 : + 53 71 : export const request = (input: RequestInput) => { + 54 22 : const { + 55 52 : system: requestSystem, + 56 22 : prompt, + 57 26 : messages, + 58 20 : tools, + 59 68 : toolChoice: requestToolChoice, + 60 68 : generation: requestGeneration, + 61 88 : providerOptions: requestProviderOptions, + 62 50 : http: requestHttp, + 63 20 : ...rest + 64 16 : } = input + 65 60 : return new LLMRequest({ + 66 18 : ...rest, + 67 92 : system: SystemPart.content(requestSystem), + 68 213 : messages: [...(messages?.map(Message.make) ?? []), ...(prompt === undefined ? [] : [Message.user(prompt)])], + 69 98 : tools: tools?.map(ToolDefinition.make) ?? [], + 70 163 : toolChoice: requestToolChoice ? ToolChoice.make(requestToolChoice) : undefined, + 71 205 : generation: requestGeneration === undefined ? undefined : GenerationOptions.make(requestGeneration), + 72 88 : providerOptions: requestProviderOptions, + 73 120 : http: requestHttp === undefined ? undefined : HttpOptions.make(requestHttp), + 74 10 : }) + 75 : } + 76 : + 77 0 : export const updateRequest = (input: LLMRequest, patch: Partial<RequestInput>) => + 78 49 : request({ ...requestInput(input), ...patch }) + 79 : + 80 104 : const GENERATE_OBJECT_TOOL_NAME = "generate_object" + 81 : + 82 188 : const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool." + 83 : + 84 : type GenerateObjectBase = Omit<RequestInput, "tools" | "toolChoice" | "responseFormat"> + 85 : + 86 : export class GenerateObjectResponse<T> { + 87 0 : constructor( + 88 0 : readonly object: T, + 89 0 : readonly response: LLMResponse, + 90 0 : ) {} + 91 0 : + 92 0 : get events() { + 93 0 : return this.response.events + 94 0 : } + 95 0 : + 96 0 : get usage() { + 97 32 : return this.response.usage + 98 : } + 99 2 : } + 100 : + 101 : export interface GenerateObjectOptions<S extends ToolSchema<any>> extends GenerateObjectBase { + 102 : readonly schema: S + 103 : } + 104 : + 105 : export interface GenerateObjectDynamicOptions extends GenerateObjectBase { + 106 : /** Raw JSON Schema object describing the expected output shape. */ + 107 : readonly jsonSchema: JsonSchema.JsonSchema + 108 : } + 109 : + 110 0 : const runGenerateObject = Effect.fn("LLM.generateObject")(function* ( + 111 0 : options: GenerateObjectBase, + 112 0 : tool: ReturnType<typeof makeTool>, + 113 0 : ) { + 114 0 : const baseRequest = request(options) + 115 0 : const generateRequest = LLMRequest.update(baseRequest, { + 116 0 : tools: toDefinitions({ [GENERATE_OBJECT_TOOL_NAME]: tool }), + 117 0 : toolChoice: ToolChoice.named(GENERATE_OBJECT_TOOL_NAME), + 118 0 : }) + 119 0 : const response = yield* LLMClient.generate(generateRequest) + 120 0 : const call = response.toolCalls.find( + 121 0 : (event) => LLMEvent.is.toolCall(event) && event.name === GENERATE_OBJECT_TOOL_NAME, + 122 0 : ) + 123 0 : if (!call || !LLMEvent.is.toolCall(call)) + 124 0 : return yield* new LLMError({ + 125 0 : module: "LLM", + 126 0 : method: "generateObject", + 127 0 : reason: new InvalidProviderOutputReason({ + 128 0 : message: `generateObject: model did not call the forced \`${GENERATE_OBJECT_TOOL_NAME}\` tool`, + 129 0 : }), + 130 0 : }) + 131 0 : const object = yield* tool._decode(call.input).pipe( + 132 0 : Effect.mapError( + 133 0 : (error) => + 134 0 : new LLMError({ + 135 0 : module: "LLM", + 136 0 : method: "generateObject", + 137 0 : reason: new InvalidProviderOutputReason({ + 138 0 : message: `generateObject: tool input failed schema decode: ${error.message}`, + 139 0 : }), + 140 0 : }), + 141 0 : ), + 142 0 : ) + 143 53 : return new GenerateObjectResponse(object, response) + 144 6 : }) + 145 : + 146 : /** + 147 : * Run a model and decode its output against `schema`. Works on every protocol + 148 : * because it forces a synthetic tool call internally — provider-native JSON + 149 : * modes are intentionally avoided so behaviour is uniform. + 150 : * + 151 : * Two input modes: + 152 : * + 153 : * 1. `schema: EffectSchema<T>` — `.object` is decoded and typed as `T`. + 154 : * Decode failures surface as `LLMError`. + 155 : * 2. `jsonSchema: JsonSchema.JsonSchema` — `.object` is `unknown`. Use when + 156 : * the schema is only available at runtime (MCP, plugin manifests). Caller validates. + 157 : */ + 158 : export function generateObject<S extends ToolSchema<any>>( + 159 : options: GenerateObjectOptions<S>, + 160 : ): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, LLMError> + 161 : export function generateObject( + 162 : options: GenerateObjectDynamicOptions, + 163 : ): Effect.Effect<GenerateObjectResponse<unknown>, LLMError> + 164 0 : export function generateObject(options: GenerateObjectOptions<ToolSchema<any>> | GenerateObjectDynamicOptions) { + 165 0 : if ("schema" in options) { + 166 0 : const { schema, ...rest } = options + 167 0 : return runGenerateObject( + 168 0 : rest, + 169 0 : makeTool({ + 170 0 : description: GENERATE_OBJECT_TOOL_DESCRIPTION, + 171 0 : parameters: schema, + 172 0 : success: Schema.Unknown as ToolSchema<unknown>, + 173 0 : execute: () => Effect.void, + 174 0 : }), + 175 0 : ) + 176 0 : } + 177 0 : const { jsonSchema, ...rest } = options + 178 0 : return runGenerateObject( + 179 0 : rest, + 180 0 : makeTool({ + 181 0 : description: GENERATE_OBJECT_TOOL_DESCRIPTION, + 182 0 : jsonSchema, + 183 0 : execute: () => Effect.void, + 184 0 : }), + 185 4 : ) + 186 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 40 : import { Effect, Schema } from "effect" + 2 40 : import { Route } from "../route/client" + 3 37 : import { Auth } from "../route/auth" + 4 45 : import { Endpoint } from "../route/endpoint" + 5 43 : import { Framing } from "../route/framing" + 6 45 : import { Protocol } from "../route/protocol" + 7 45 : import { + 8 : LLMEvent, + 9 : Usage, + 10 : type CacheHint, + 11 : type FinishReason, + 12 : type JsonSchema, + 13 : type LLMRequest, + 14 : type MediaPart, + 15 : type ProviderMetadata, + 16 : type ToolCallPart, + 17 : type ToolDefinition, + 18 : type ToolContent, + 19 : type ToolResultPart, + 20 : } from "../schema" + 21 83 : import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" + 22 54 : import { isContextOverflow } from "../provider-error" + 23 39 : import * as Cache from "./utils/cache" + 24 46 : import { Lifecycle } from "./utils/lifecycle" + 25 59 : import { ToolSchemaProjection } from "./utils/tool-schema" + 26 49 : import { ToolStream } from "./utils/tool-stream" + 27 : + 28 37 : const ADAPTER = "anthropic-messages" + 29 63 : export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1" + 30 32 : export const PATH = "/messages" + 31 : + 32 : // ============================================================================= + 33 : // Request Body Schema + 34 : // ============================================================================= + 35 46 : const AnthropicCacheControl = Schema.Struct({ + 36 32 : type: Schema.tag("ephemeral"), + 37 52 : ttl: Schema.optional(Schema.Literals(["5m", "1h"])), + 38 3 : }) + 39 : + 40 43 : const AnthropicTextBlock = Schema.Struct({ + 41 27 : type: Schema.tag("text"), + 42 22 : text: Schema.String, + 43 54 : cache_control: Schema.optional(AnthropicCacheControl), + 44 3 : }) + 45 : type AnthropicTextBlock = Schema.Schema.Type<typeof AnthropicTextBlock> + 46 : + 47 44 : const AnthropicImageBlock = Schema.Struct({ + 48 28 : type: Schema.tag("image"), + 49 27 : source: Schema.Struct({ + 50 31 : type: Schema.tag("base64"), + 51 30 : media_type: Schema.String, + 52 21 : data: Schema.String, + 53 5 : }), + 54 54 : cache_control: Schema.optional(AnthropicCacheControl), + 55 3 : }) + 56 : type AnthropicImageBlock = Schema.Schema.Type<typeof AnthropicImageBlock> + 57 : + 58 47 : const AnthropicThinkingBlock = Schema.Struct({ + 59 31 : type: Schema.tag("thinking"), + 60 26 : thinking: Schema.String, + 61 44 : signature: Schema.optional(Schema.String), + 62 54 : cache_control: Schema.optional(AnthropicCacheControl), + 63 3 : }) + 64 : + 65 46 : const AnthropicToolUseBlock = Schema.Struct({ + 66 31 : type: Schema.tag("tool_use"), + 67 20 : id: Schema.String, + 68 22 : name: Schema.String, + 69 24 : input: Schema.Unknown, + 70 54 : cache_control: Schema.optional(AnthropicCacheControl), + 71 3 : }) + 72 : type AnthropicToolUseBlock = Schema.Schema.Type<typeof AnthropicToolUseBlock> + 73 : + 74 52 : const AnthropicServerToolUseBlock = Schema.Struct({ + 75 38 : type: Schema.tag("server_tool_use"), + 76 20 : id: Schema.String, + 77 22 : name: Schema.String, + 78 24 : input: Schema.Unknown, + 79 54 : cache_control: Schema.optional(AnthropicCacheControl), + 80 3 : }) + 81 : type AnthropicServerToolUseBlock = Schema.Schema.Type<typeof AnthropicServerToolUseBlock> + 82 : + 83 : // Server tool result blocks: web_search_tool_result, code_execution_tool_result, + 84 : // and web_fetch_tool_result. The provider executes the tool and inlines the + 85 : // structured result into the assistant turn — there is no client tool_result + 86 : // round-trip. We round-trip the structured `content` payload as opaque JSON so + 87 : // the next request can echo it back when continuing the conversation. + 88 56 : const AnthropicServerToolResultType = Schema.Literals([ + 89 27 : "web_search_tool_result", + 90 31 : "code_execution_tool_result", + 91 24 : "web_fetch_tool_result", + 92 3 : ]) + 93 : type AnthropicServerToolResultType = Schema.Schema.Type<typeof AnthropicServerToolResultType> + 94 : + 95 55 : const AnthropicServerToolResultBlock = Schema.Struct({ + 96 38 : type: AnthropicServerToolResultType, + 97 29 : tool_use_id: Schema.String, + 98 26 : content: Schema.Unknown, + 99 54 : cache_control: Schema.optional(AnthropicCacheControl), + 100 3 : }) + 101 : type AnthropicServerToolResultBlock = Schema.Schema.Type<typeof AnthropicServerToolResultBlock> + 102 : + 103 : // Anthropic accepts either a plain string or an ordered array of text/image + 104 : // blocks inside `tool_result.content`. The array form is required when a tool + 105 : // returns image bytes (screenshot, image search, etc.) so they can be passed + 106 : // to the model as proper image inputs instead of being JSON-stringified into + 107 : // the prompt — which silently inflates context by megabytes and can push the + 108 : // conversation over the model's token limit. + 109 91 : const AnthropicToolResultContent = Schema.Union([AnthropicTextBlock, AnthropicImageBlock]) + 110 : + 111 49 : const AnthropicToolResultBlock = Schema.Struct({ + 112 34 : type: Schema.tag("tool_result"), + 113 29 : tool_use_id: Schema.String, + 114 83 : content: Schema.Union([Schema.String, Schema.Array(AnthropicToolResultContent)]), + 115 44 : is_error: Schema.optional(Schema.Boolean), + 116 54 : cache_control: Schema.optional(AnthropicCacheControl), + 117 3 : }) + 118 : + 119 109 : const AnthropicUserBlock = Schema.Union([AnthropicTextBlock, AnthropicImageBlock, AnthropicToolResultBlock]) + 120 : type AnthropicUserBlock = Schema.Schema.Type<typeof AnthropicUserBlock> + 121 47 : const AnthropicAssistantBlock = Schema.Union([ + 122 21 : AnthropicTextBlock, + 123 25 : AnthropicThinkingBlock, + 124 24 : AnthropicToolUseBlock, + 125 30 : AnthropicServerToolUseBlock, + 126 31 : AnthropicServerToolResultBlock, + 127 3 : ]) + 128 : type AnthropicAssistantBlock = Schema.Schema.Type<typeof AnthropicAssistantBlock> + 129 : type AnthropicToolResultBlock = Schema.Schema.Type<typeof AnthropicToolResultBlock> + 130 : + 131 40 : const AnthropicMessage = Schema.Union([ + 132 93 : Schema.Struct({ role: Schema.Literal("user"), content: Schema.Array(AnthropicUserBlock) }), + 133 103 : Schema.Struct({ role: Schema.Literal("assistant"), content: Schema.Array(AnthropicAssistantBlock) }), + 134 93 : Schema.Struct({ role: Schema.Literal("system"), content: Schema.Array(AnthropicTextBlock) }), + 135 38 : ]).pipe(Schema.toTaggedUnion("role")) + 136 : type AnthropicMessage = Schema.Schema.Type<typeof AnthropicMessage> + 137 : + 138 38 : const AnthropicTool = Schema.Struct({ + 139 22 : name: Schema.String, + 140 29 : description: Schema.String, + 141 27 : input_schema: JsonObject, + 142 54 : cache_control: Schema.optional(AnthropicCacheControl), + 143 3 : }) + 144 : type AnthropicTool = Schema.Schema.Type<typeof AnthropicTool> + 145 : + 146 43 : const AnthropicToolChoice = Schema.Union([ + 147 60 : Schema.Struct({ type: Schema.Literals(["auto", "any"]) }), + 148 65 : Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }), + 149 3 : ]) + 150 : + 151 42 : const AnthropicThinking = Schema.Struct({ + 152 30 : type: Schema.tag("enabled"), + 153 29 : budget_tokens: Schema.Number, + 154 3 : }) + 155 : + 156 30 : const AnthropicBodyFields = { + 157 23 : model: Schema.String, + 158 44 : system: optionalArray(AnthropicTextBlock), + 159 43 : messages: Schema.Array(AnthropicMessage), + 160 38 : tools: optionalArray(AnthropicTool), + 161 52 : tool_choice: Schema.optional(AnthropicToolChoice), + 162 31 : stream: Schema.Literal(true), + 163 28 : max_tokens: Schema.Number, + 164 46 : temperature: Schema.optional(Schema.Number), + 165 40 : top_p: Schema.optional(Schema.Number), + 166 40 : top_k: Schema.optional(Schema.Number), + 167 47 : stop_sequences: optionalArray(Schema.String), + 168 45 : thinking: Schema.optional(AnthropicThinking), + 169 2 : } + 170 65 : const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields) + 171 : export type AnthropicMessagesBody = Schema.Schema.Type<typeof AnthropicMessagesBody> + 172 : + 173 39 : const AnthropicUsage = Schema.Struct({ + 174 47 : input_tokens: Schema.optional(Schema.Number), + 175 48 : output_tokens: Schema.optional(Schema.Number), + 176 59 : cache_creation_input_tokens: optionalNull(Schema.Number), + 177 53 : cache_read_input_tokens: optionalNull(Schema.Number), + 178 3 : }) + 179 : type AnthropicUsage = Schema.Schema.Type<typeof AnthropicUsage> + 180 : + 181 45 : const AnthropicStreamBlock = Schema.Struct({ + 182 22 : type: Schema.String, + 183 37 : id: Schema.optional(Schema.String), + 184 39 : name: Schema.optional(Schema.String), + 185 39 : text: Schema.optional(Schema.String), + 186 43 : thinking: Schema.optional(Schema.String), + 187 44 : signature: Schema.optional(Schema.String), + 188 41 : input: Schema.optional(Schema.Unknown), + 189 : // *_tool_result blocks arrive whole as content_block_start (no streaming + 190 : // delta) with the structured payload in `content` and the originating + 191 : // server_tool_use id in `tool_use_id`. + 192 46 : tool_use_id: Schema.optional(Schema.String), + 193 41 : content: Schema.optional(Schema.Unknown), + 194 3 : }) + 195 : + 196 45 : const AnthropicStreamDelta = Schema.Struct({ + 197 39 : type: Schema.optional(Schema.String), + 198 39 : text: Schema.optional(Schema.String), + 199 43 : thinking: Schema.optional(Schema.String), + 200 47 : partial_json: Schema.optional(Schema.String), + 201 44 : signature: Schema.optional(Schema.String), + 202 43 : stop_reason: optionalNull(Schema.String), + 203 43 : stop_sequence: optionalNull(Schema.String), + 204 3 : }) + 205 : + 206 39 : const AnthropicEvent = Schema.Struct({ + 207 22 : type: Schema.String, + 208 40 : index: Schema.optional(Schema.Number), + 209 86 : message: Schema.optional(Schema.Struct({ usage: Schema.optional(AnthropicUsage) })), + 210 55 : content_block: Schema.optional(AnthropicStreamBlock), + 211 47 : delta: Schema.optional(AnthropicStreamDelta), + 212 41 : usage: Schema.optional(AnthropicUsage), + 213 : // `type` and `message` are both required per Anthropic's spec, but + 214 : // OpenAI-compatible proxies and gateway translations occasionally drop one + 215 : // or the other; mark them optional so a partial payload still parses and + 216 : // the parser can fall back to whichever field is populated. + 217 23 : error: Schema.optional( + 218 96 : Schema.Struct({ type: Schema.optional(Schema.String), message: Schema.optional(Schema.String) }), + 219 2 : ), + 220 3 : }) + 221 : type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent> + 222 : + 223 : interface ParserState { + 224 : readonly tools: ToolStream.State<number> + 225 : readonly usage?: Usage + 226 : readonly lifecycle: Lifecycle.State + 227 : } + 228 : + 229 46 : const invalid = ProviderShared.invalidRequest + 230 : + 231 : // ============================================================================= + 232 : // Request Lowering + 233 : // ============================================================================= + 234 : // Anthropic accepts at most 4 explicit cache_control breakpoints per request, + 235 : // across `tools`, `system`, and `messages`. Beyond the cap the API returns a + 236 : // 400 — so the lowering layer counts emitted markers and silently drops any + 237 : // that exceed it. + 238 35 : const ANTHROPIC_BREAKPOINT_CAP = 4 + 239 : + 240 43 : const EPHEMERAL_5M = { type: "ephemeral" as const } + 241 54 : const EPHEMERAL_1H = { type: "ephemeral" as const, ttl: "1h" as const } + 242 : + 243 0 : const cacheControl = (breakpoints: Cache.Breakpoints, cache: CacheHint | undefined) => { + 244 0 : if (cache?.type !== "ephemeral" && cache?.type !== "persistent") return undefined + 245 0 : if (breakpoints.remaining <= 0) { + 246 0 : breakpoints.dropped += 1 + 247 0 : return undefined + 248 0 : } + 249 0 : breakpoints.remaining -= 1 + 250 2 : return Cache.ttlBucket(cache.ttlSeconds) === "1h" ? EPHEMERAL_1H : EPHEMERAL_5M + 251 : } + 252 : + 253 26 : const anthropicMetadata = (metadata: Record<string, unknown>): ProviderMetadata => ({ anthropic: metadata }) + 254 : + 255 0 : const signatureFromMetadata = (metadata: ProviderMetadata | undefined): string | undefined => { + 256 0 : const anthropic = metadata?.anthropic + 257 0 : if (!ProviderShared.isRecord(anthropic)) return undefined + 258 2 : return typeof anthropic.signature === "string" ? anthropic.signature : undefined + 259 : } + 260 : + 261 0 : const lowerTool = (breakpoints: Cache.Breakpoints, tool: ToolDefinition, inputSchema: JsonSchema): AnthropicTool => ({ + 262 0 : name: tool.name, + 263 0 : description: tool.description, + 264 0 : input_schema: inputSchema, + 265 : cache_control: cacheControl(breakpoints, tool.cache), + 266 2 : }) + 267 : + 268 0 : const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) => + 269 0 : ProviderShared.matchToolChoice("Anthropic Messages", toolChoice, { + 270 0 : auto: () => ({ type: "auto" as const }), + 271 0 : none: () => undefined, + 272 0 : required: () => ({ type: "any" as const }), + 273 : tool: (name) => ({ type: "tool" as const, name }), + 274 2 : }) + 275 : + 276 0 : const lowerToolCall = (part: ToolCallPart): AnthropicToolUseBlock => ({ + 277 0 : type: "tool_use", + 278 0 : id: part.id, + 279 0 : name: part.name, + 280 : input: part.input, + 281 2 : }) + 282 : + 283 0 : const lowerServerToolCall = (part: ToolCallPart): AnthropicServerToolUseBlock => ({ + 284 0 : type: "server_tool_use", + 285 0 : id: part.id, + 286 0 : name: part.name, + 287 : input: part.input, + 288 2 : }) + 289 : + 290 : // Server tool result blocks are typed by name. Anthropic ships three today; + 291 : // extend this list when new server tools land. The block content is the + 292 : // structured payload returned by the provider, which we round-trip as-is. + 293 0 : const serverToolResultType = (name: string): AnthropicServerToolResultType | undefined => { + 294 0 : if (name === "web_search") return "web_search_tool_result" + 295 0 : if (name === "code_execution") return "code_execution_tool_result" + 296 0 : if (name === "web_fetch") return "web_fetch_tool_result" + 297 2 : return undefined + 298 : } + 299 : + 300 0 : const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult")(function* (part: ToolResultPart) { + 301 0 : const wireType = serverToolResultType(part.name) + 302 0 : if (!wireType) + 303 0 : return yield* invalid(`Anthropic Messages does not know how to round-trip server tool result for ${part.name}`) + 304 : return { type: wireType, tool_use_id: part.id, content: part.result.value } satisfies AnthropicServerToolResultBlock + 305 3 : }) + 306 : + 307 0 : const lowerImage = Effect.fn("AnthropicMessages.lowerImage")(function* (part: MediaPart) { + 308 0 : const media = yield* ProviderShared.validateMedia( + 309 0 : "Anthropic Messages", + 310 0 : part, + 311 0 : new Set<string>(ProviderShared.IMAGE_MIMES), + 312 0 : ) + 313 0 : return { + 314 0 : type: "image" as const, + 315 0 : source: { + 316 0 : type: "base64" as const, + 317 0 : media_type: media.mime, + 318 0 : data: media.base64, + 319 0 : }, + 320 : } satisfies AnthropicImageBlock + 321 3 : }) + 322 : + 323 : // Tool results may carry structured text/images. Keep media as provider-native + 324 : // content instead of JSON-stringifying base64 into a prompt string. + 325 0 : const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* ( + 326 0 : item: ToolContent, + 327 0 : ) { + 328 0 : if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock + 329 0 : const media = yield* ProviderShared.validateToolFile( + 330 0 : "Anthropic Messages", + 331 0 : item, + 332 0 : new Set<string>(ProviderShared.IMAGE_MIMES), + 333 0 : ) + 334 0 : return { + 335 0 : type: "image" as const, + 336 0 : source: { + 337 0 : type: "base64" as const, + 338 0 : media_type: media.mime, + 339 0 : data: media.base64, + 340 0 : }, + 341 : } satisfies AnthropicImageBlock + 342 3 : }) + 343 : + 344 0 : const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultContent")(function* (part: ToolResultPart) { + 345 0 : // Text / json / error results stay as a string for backward compatibility + 346 0 : // with existing cassettes and provider expectations. + 347 0 : if (part.result.type !== "content") return ProviderShared.toolResultText(part) + 348 0 : // Preserve the narrowed array element type when compiled through a consumer package. + 349 0 : const content: ReadonlyArray<ToolContent> = part.result.value + 350 : return yield* Effect.forEach(content, lowerToolResultContentItem) + 351 3 : }) + 352 : + 353 : // Mid-conversation system messages are a native Claude API feature only for + 354 : // Opus 4.8. Other Anthropic models intentionally use the same visible wrapped- + 355 : // user fallback as non-Anthropic routes rather than sending a role they reject. + 356 36 : const supportsNativeSystemUpdates = (request: LLMRequest) => String(request.model.id) === "claude-opus-4-8" + 357 : + 358 0 : const endsInServerToolUse = (message: LLMRequest["messages"][number]) => { + 359 0 : const last = message.content.at(-1) + 360 2 : return message.role === "assistant" && last?.type === "tool-call" && last.providerExecuted === true + 361 : } + 362 : + 363 0 : const canUseNativeSystemUpdate = (messages: LLMRequest["messages"], index: number) => { + 364 0 : const previous = messages[index - 1] + 365 0 : const next = messages[index + 1] + 366 0 : return ( + 367 0 : previous !== undefined && + 368 0 : previous.role !== "system" && + 369 0 : (previous.role === "user" || previous.role === "tool" || endsInServerToolUse(previous)) && + 370 0 : next?.role !== "system" && + 371 2 : (next === undefined || next.role === "assistant") + 372 : ) + 373 : } + 374 : + 375 0 : const splitsLocalToolResults = (messages: LLMRequest["messages"], index: number) => { + 376 0 : const pending = new Set<string>() + 377 0 : for (const message of messages.slice(0, index)) { + 378 0 : for (const part of message.content) { + 379 0 : if (message.role === "assistant" && part.type === "tool-call" && part.providerExecuted !== true) + 380 0 : pending.add(part.id) + 381 0 : if (message.role === "tool" && part.type === "tool-result") pending.delete(part.id) + 382 0 : } + 383 0 : } + 384 2 : return pending.size > 0 + 385 : } + 386 : + 387 0 : const lowerNativeSystemUpdate = Effect.fn("AnthropicMessages.lowerNativeSystemUpdate")(function* ( + 388 0 : message: LLMRequest["messages"][number], + 389 0 : breakpoints: Cache.Breakpoints, + 390 0 : ) { + 391 0 : const content = yield* ProviderShared.systemUpdateText("Anthropic Messages", message) + 392 0 : return { + 393 0 : role: "system" as const, + 394 0 : content: content.map((part) => ({ + 395 0 : type: "text" as const, + 396 0 : text: part.text, + 397 0 : cache_control: cacheControl(breakpoints, part.cache), + 398 0 : })), + 399 : } + 400 3 : }) + 401 : + 402 0 : const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* ( + 403 0 : request: LLMRequest, + 404 0 : breakpoints: Cache.Breakpoints, + 405 0 : ) { + 406 0 : const messages: AnthropicMessage[] = [] + 407 0 : + 408 0 : for (const [index, message] of request.messages.entries()) { + 409 0 : if (message.role === "system") { + 410 0 : if (splitsLocalToolResults(request.messages, index)) + 411 0 : return yield* invalid("Anthropic Messages system updates cannot split a local tool call from its tool result") + 412 0 : if (supportsNativeSystemUpdates(request) && canUseNativeSystemUpdate(request.messages, index)) { + 413 0 : messages.push(yield* lowerNativeSystemUpdate(message, breakpoints)) + 414 0 : continue + 415 0 : } + 416 0 : const part = yield* ProviderShared.wrappedSystemUpdate("Anthropic Messages", message) + 417 0 : const block = { type: "text" as const, text: part.text, cache_control: cacheControl(breakpoints, part.cache) } + 418 0 : const previous = messages.at(-1) + 419 0 : if (previous?.role === "user") + 420 0 : messages[messages.length - 1] = { role: "user", content: [...previous.content, block] } + 421 0 : else messages.push({ role: "user", content: [block] }) + 422 0 : continue + 423 0 : } + 424 0 : + 425 0 : if (message.role === "user") { + 426 0 : const content: AnthropicUserBlock[] = [] + 427 0 : for (const part of message.content) { + 428 0 : if (part.type === "text") { + 429 0 : content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) }) + 430 0 : continue + 431 0 : } + 432 0 : if (part.type === "media") { + 433 0 : content.push(yield* lowerImage(part)) + 434 0 : continue + 435 0 : } + 436 0 : return yield* ProviderShared.unsupportedContent("Anthropic Messages", "user", ["text", "media"]) + 437 0 : } + 438 0 : messages.push({ role: "user", content }) + 439 0 : continue + 440 0 : } + 441 0 : + 442 0 : if (message.role === "assistant") { + 443 0 : const content: AnthropicAssistantBlock[] = [] + 444 0 : for (const part of message.content) { + 445 0 : if (part.type === "text") { + 446 0 : content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) }) + 447 0 : continue + 448 0 : } + 449 0 : if (part.type === "reasoning") { + 450 0 : content.push({ + 451 0 : type: "thinking", + 452 0 : thinking: part.text, + 453 0 : signature: part.encrypted ?? signatureFromMetadata(part.providerMetadata), + 454 0 : }) + 455 0 : continue + 456 0 : } + 457 0 : if (part.type === "tool-call") { + 458 0 : content.push(part.providerExecuted ? lowerServerToolCall(part) : lowerToolCall(part)) + 459 0 : continue + 460 0 : } + 461 0 : if (part.type === "tool-result" && part.providerExecuted) { + 462 0 : content.push(yield* lowerServerToolResult(part)) + 463 0 : continue + 464 0 : } + 465 0 : return yield* invalid( + 466 0 : `Anthropic Messages assistant messages only support text, reasoning, and tool-call content for now`, + 467 0 : ) + 468 0 : } + 469 0 : messages.push({ role: "assistant", content }) + 470 0 : continue + 471 0 : } + 472 0 : + 473 0 : const content: AnthropicToolResultBlock[] = [] + 474 0 : for (const part of message.content) { + 475 0 : if (!ProviderShared.supportsContent(part, ["tool-result"])) + 476 0 : return yield* ProviderShared.unsupportedContent("Anthropic Messages", "tool", ["tool-result"]) + 477 0 : content.push({ + 478 0 : type: "tool_result", + 479 0 : tool_use_id: part.id, + 480 0 : content: yield* lowerToolResultContent(part), + 481 0 : is_error: part.result.type === "error" ? true : undefined, + 482 0 : cache_control: cacheControl(breakpoints, part.cache), + 483 0 : }) + 484 0 : } + 485 0 : messages.push({ role: "user", content }) + 486 0 : } + 487 0 : + 488 : return messages + 489 3 : }) + 490 : + 491 25 : const anthropicOptions = (request: LLMRequest) => request.providerOptions?.anthropic + 492 : + 493 0 : const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (request: LLMRequest) { + 494 0 : const thinking = anthropicOptions(request)?.thinking + 495 0 : if (!ProviderShared.isRecord(thinking) || thinking.type !== "enabled") return undefined + 496 0 : const budget = + 497 0 : typeof thinking.budgetTokens === "number" + 498 0 : ? thinking.budgetTokens + 499 0 : : typeof thinking.budget_tokens === "number" + 500 0 : ? thinking.budget_tokens + 501 0 : : undefined + 502 0 : if (budget === undefined) return yield* invalid("Anthropic thinking provider option requires budgetTokens") + 503 : return { type: "enabled" as const, budget_tokens: budget } + 504 3 : }) + 505 : + 506 0 : const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) { + 507 0 : const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined + 508 0 : const generation = request.generation + 509 0 : const toolSchemaCompatibility = request.model.compatibility?.toolSchema + 510 0 : const outputLimit = request.model.defaults?.limits?.output ?? request.model.route.defaults.limits?.output ?? 4096 + 511 0 : // Allocate the 4-breakpoint budget in invalidation order: tools → system → + 512 0 : // messages. Tools live highest in the cache hierarchy, so when callers + 513 0 : // over-mark we keep their tool hints and shed the message-tail ones first. + 514 0 : const breakpoints = Cache.newBreakpoints(ANTHROPIC_BREAKPOINT_CAP) + 515 0 : const tools = + 516 0 : request.tools.length === 0 || request.toolChoice?.type === "none" + 517 0 : ? undefined + 518 0 : : request.tools.map((tool) => + 519 0 : lowerTool( + 520 0 : breakpoints, + 521 0 : tool, + 522 0 : ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility), + 523 0 : ), + 524 0 : ) + 525 0 : const system = + 526 0 : request.system.length === 0 + 527 0 : ? undefined + 528 0 : : request.system.map((part) => ({ + 529 0 : type: "text" as const, + 530 0 : text: part.text, + 531 0 : cache_control: cacheControl(breakpoints, part.cache), + 532 0 : })) + 533 0 : const messages = yield* lowerMessages(request, breakpoints) + 534 0 : if (breakpoints.dropped > 0) { + 535 0 : yield* Effect.logWarning( + 536 0 : `Anthropic Messages: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${ANTHROPIC_BREAKPOINT_CAP} per request.`, + 537 0 : ) + 538 0 : } + 539 0 : return { + 540 0 : model: request.model.id, + 541 0 : system, + 542 0 : messages, + 543 0 : tools, + 544 0 : tool_choice: toolChoice, + 545 0 : stream: true as const, + 546 0 : max_tokens: generation?.maxTokens ?? outputLimit, + 547 0 : temperature: generation?.temperature, + 548 0 : top_p: generation?.topP, + 549 0 : top_k: generation?.topK, + 550 0 : stop_sequences: generation?.stop, + 551 0 : thinking: yield* lowerThinking(request), + 552 : } + 553 3 : }) + 554 : + 555 : // ============================================================================= + 556 : // Stream Parsing + 557 : // ============================================================================= + 558 0 : const mapFinishReason = (reason: string | null | undefined): FinishReason => { + 559 0 : if (reason === "end_turn" || reason === "stop_sequence" || reason === "pause_turn") return "stop" + 560 0 : if (reason === "max_tokens") return "length" + 561 0 : if (reason === "tool_use") return "tool-calls" + 562 0 : if (reason === "refusal") return "content-filter" + 563 2 : return "unknown" + 564 : } + 565 : + 566 : // Anthropic reports the non-overlapping breakdown natively — its + 567 : // `input_tokens` is the *non-cached* count per the Messages API docs, with + 568 : // cache reads and writes as separate fields. We sum them to derive the + 569 : // inclusive `inputTokens` the rest of the contract expects. Extended + 570 : // thinking tokens are *not* broken out by Anthropic — they're billed as + 571 : // part of `output_tokens`, so `reasoningTokens` stays `undefined` and + 572 : // `outputTokens` carries the combined total. + 573 0 : const mapUsage = (usage: AnthropicUsage | undefined): Usage | undefined => { + 574 0 : if (!usage) return undefined + 575 0 : const nonCached = usage.input_tokens + 576 0 : const cacheRead = usage.cache_read_input_tokens ?? undefined + 577 0 : const cacheWrite = usage.cache_creation_input_tokens ?? undefined + 578 0 : const inputTokens = ProviderShared.sumTokens(nonCached, cacheRead, cacheWrite) + 579 0 : return new Usage({ + 580 0 : inputTokens, + 581 0 : outputTokens: usage.output_tokens, + 582 0 : nonCachedInputTokens: nonCached, + 583 0 : cacheReadInputTokens: cacheRead, + 584 0 : cacheWriteInputTokens: cacheWrite, + 585 0 : totalTokens: ProviderShared.totalTokens(inputTokens, usage.output_tokens, undefined), + 586 0 : providerMetadata: { anthropic: usage }, + 587 2 : }) + 588 : } + 589 : + 590 : // Anthropic emits usage on `message_start` and again on `message_delta` — the + 591 : // final delta carries the authoritative totals. Right-biased merge: each + 592 : // field prefers `right` when defined, falls back to `left`. `inputTokens` is + 593 : // recomputed from the merged breakdown so the inclusive total stays + 594 : // consistent with `nonCached + cacheRead + cacheWrite`. + 595 0 : const mergeUsage = (left: Usage | undefined, right: Usage | undefined) => { + 596 0 : if (!left) return right + 597 0 : if (!right) return left + 598 0 : const nonCachedInputTokens = right.nonCachedInputTokens ?? left.nonCachedInputTokens + 599 0 : const cacheReadInputTokens = right.cacheReadInputTokens ?? left.cacheReadInputTokens + 600 0 : const cacheWriteInputTokens = right.cacheWriteInputTokens ?? left.cacheWriteInputTokens + 601 0 : const inputTokens = ProviderShared.sumTokens(nonCachedInputTokens, cacheReadInputTokens, cacheWriteInputTokens) + 602 0 : const outputTokens = right.outputTokens ?? left.outputTokens + 603 0 : return new Usage({ + 604 0 : inputTokens, + 605 0 : outputTokens, + 606 0 : nonCachedInputTokens, + 607 0 : cacheReadInputTokens, + 608 0 : cacheWriteInputTokens, + 609 0 : totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined), + 610 0 : providerMetadata: { + 611 0 : anthropic: { + 612 0 : ...left.providerMetadata?.["anthropic"], + 613 0 : ...right.providerMetadata?.["anthropic"], + 614 0 : }, + 615 0 : }, + 616 2 : }) + 617 : } + 618 : + 619 : // Server tool result blocks come whole in `content_block_start` (no streaming + 620 : // delta sequence). We convert the payload to a `tool-result` event with + 621 : // `providerExecuted: true`. The runtime appends it to the assistant message + 622 : // for round-trip; downstream consumers can inspect `result.value` for the + 623 : // structured payload. + 624 35 : const SERVER_TOOL_RESULT_NAMES: Record<AnthropicServerToolResultType, string> = { + 625 39 : web_search_tool_result: "web_search", + 626 47 : code_execution_tool_result: "code_execution", + 627 35 : web_fetch_tool_result: "web_fetch", + 628 2 : } + 629 : + 630 31 : const isServerToolResultType = (type: string): type is AnthropicServerToolResultType => type in SERVER_TOOL_RESULT_NAMES + 631 : + 632 0 : const serverToolResultEvent = (block: NonNullable<AnthropicEvent["content_block"]>): LLMEvent | undefined => { + 633 0 : if (!block.type || !isServerToolResultType(block.type)) return undefined + 634 0 : const errorPayload = + 635 0 : typeof block.content === "object" && block.content !== null && "type" in block.content + 636 0 : ? String((block.content as Record<string, unknown>).type) + 637 0 : : "" + 638 0 : const isError = errorPayload.endsWith("_tool_result_error") + 639 0 : return LLMEvent.toolResult({ + 640 0 : id: block.tool_use_id ?? "", + 641 0 : name: SERVER_TOOL_RESULT_NAMES[block.type], + 642 0 : result: isError ? { type: "error", value: block.content } : { type: "json", value: block.content }, + 643 0 : providerExecuted: true, + 644 0 : providerMetadata: anthropicMetadata({ blockType: block.type }), + 645 2 : }) + 646 : } + 647 : + 648 : type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>] + 649 : + 650 21 : const NO_EVENTS: StepResult["1"] = [] + 651 : + 652 0 : const onMessageStart = (state: ParserState, event: AnthropicEvent): StepResult => { + 653 0 : const usage = mapUsage(event.message?.usage) + 654 2 : return [usage ? { ...state, usage: mergeUsage(state.usage, usage) } : state, NO_EVENTS] + 655 : } + 656 : + 657 0 : const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepResult => { + 658 0 : const block = event.content_block + 659 0 : if (!block) return [state, NO_EVENTS] + 660 0 : + 661 0 : if ((block.type === "tool_use" || block.type === "server_tool_use") && event.index !== undefined) { + 662 0 : const events: LLMEvent[] = [] + 663 0 : const lifecycle = Lifecycle.stepStart(state.lifecycle, events) + 664 0 : return [ + 665 0 : { + 666 0 : ...state, + 667 0 : lifecycle, + 668 0 : tools: ToolStream.start(state.tools, event.index, { + 669 0 : id: block.id ?? String(event.index), + 670 0 : name: block.name ?? "", + 671 0 : providerExecuted: block.type === "server_tool_use", + 672 0 : }), + 673 0 : }, + 674 0 : [...events, LLMEvent.toolInputStart({ id: block.id ?? String(event.index), name: block.name ?? "" })], + 675 0 : ] + 676 0 : } + 677 0 : + 678 0 : if (block.type === "text" && block.text) { + 679 0 : const events: LLMEvent[] = [] + 680 0 : return [ + 681 0 : { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, `text-${event.index ?? 0}`, block.text) }, + 682 0 : events, + 683 0 : ] + 684 0 : } + 685 0 : + 686 0 : if (block.type === "thinking" && block.thinking) { + 687 0 : const events: LLMEvent[] = [] + 688 0 : return [ + 689 0 : { + 690 0 : ...state, + 691 0 : lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${event.index ?? 0}`, block.thinking), + 692 0 : }, + 693 0 : events, + 694 0 : ] + 695 0 : } + 696 0 : + 697 0 : const result = serverToolResultEvent(block) + 698 0 : if (!result) return [state, NO_EVENTS] + 699 0 : const events: LLMEvent[] = [] + 700 2 : return [{ ...state, lifecycle: Lifecycle.stepStart(state.lifecycle, events) }, [...events, result]] + 701 : } + 702 : + 703 0 : const onContentBlockDelta = Effect.fn("AnthropicMessages.onContentBlockDelta")(function* ( + 704 0 : state: ParserState, + 705 0 : event: AnthropicEvent, + 706 0 : ) { + 707 0 : const delta = event.delta + 708 0 : + 709 0 : if (delta?.type === "text_delta" && delta.text) { + 710 0 : const events: LLMEvent[] = [] + 711 0 : return [ + 712 0 : { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, `text-${event.index ?? 0}`, delta.text) }, + 713 0 : events, + 714 0 : ] satisfies StepResult + 715 0 : } + 716 0 : + 717 0 : if (delta?.type === "thinking_delta" && delta.thinking) { + 718 0 : const events: LLMEvent[] = [] + 719 0 : return [ + 720 0 : { + 721 0 : ...state, + 722 0 : lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${event.index ?? 0}`, delta.thinking), + 723 0 : }, + 724 0 : events, + 725 0 : ] satisfies StepResult + 726 0 : } + 727 0 : + 728 0 : if (delta?.type === "signature_delta" && delta.signature) { + 729 0 : const events: LLMEvent[] = [] + 730 0 : return [ + 731 0 : { + 732 0 : ...state, + 733 0 : lifecycle: Lifecycle.reasoningEnd( + 734 0 : state.lifecycle, + 735 0 : events, + 736 0 : `reasoning-${event.index ?? 0}`, + 737 0 : anthropicMetadata({ signature: delta.signature }), + 738 0 : ), + 739 0 : }, + 740 0 : events, + 741 0 : ] satisfies StepResult + 742 0 : } + 743 0 : + 744 0 : if (delta?.type === "input_json_delta" && event.index !== undefined) { + 745 0 : if (!delta.partial_json) return [state, NO_EVENTS] satisfies StepResult + 746 0 : const result = ToolStream.appendExisting( + 747 0 : ADAPTER, + 748 0 : state.tools, + 749 0 : event.index, + 750 0 : delta.partial_json, + 751 0 : "Anthropic Messages tool argument delta is missing its tool call", + 752 0 : ) + 753 0 : if (ToolStream.isError(result)) return yield* result + 754 0 : const events: LLMEvent[] = [] + 755 0 : const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle + 756 0 : events.push(...result.events) + 757 0 : return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult + 758 0 : } + 759 0 : + 760 : return [state, NO_EVENTS] satisfies StepResult + 761 3 : }) + 762 : + 763 0 : const onContentBlockStop = Effect.fn("AnthropicMessages.onContentBlockStop")(function* ( + 764 0 : state: ParserState, + 765 0 : event: AnthropicEvent, + 766 0 : ) { + 767 0 : if (event.index === undefined) return [state, NO_EVENTS] satisfies StepResult + 768 0 : const result = yield* ToolStream.finish(ADAPTER, state.tools, event.index) + 769 0 : const events: LLMEvent[] = [] + 770 0 : const resultEvents = result.events ?? [] + 771 0 : const lifecycle = resultEvents.length + 772 0 : ? Lifecycle.stepStart(state.lifecycle, events) + 773 0 : : Lifecycle.reasoningEnd( + 774 0 : Lifecycle.textEnd(state.lifecycle, events, `text-${event.index}`), + 775 0 : events, + 776 0 : `reasoning-${event.index}`, + 777 0 : ) + 778 0 : events.push(...resultEvents) + 779 : return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult + 780 3 : }) + 781 : + 782 0 : const onMessageDelta = (state: ParserState, event: AnthropicEvent): StepResult => { + 783 0 : const usage = mergeUsage(state.usage, mapUsage(event.usage)) + 784 0 : const events: LLMEvent[] = [] + 785 0 : const lifecycle = Lifecycle.finish(state.lifecycle, events, { + 786 0 : reason: mapFinishReason(event.delta?.stop_reason), + 787 0 : usage, + 788 0 : providerMetadata: event.delta?.stop_sequence + 789 0 : ? anthropicMetadata({ stopSequence: event.delta.stop_sequence }) + 790 0 : : undefined, + 791 0 : }) + 792 2 : return [{ ...state, lifecycle, usage }, events] + 793 : } + 794 : + 795 : // Prefix `error.type` so overloads, rate limits, and quota errors are visible + 796 : // even when the provider message is generic or empty. + 797 0 : const providerErrorMessage = (event: AnthropicEvent): string => { + 798 0 : const type = event.error?.type + 799 0 : const message = event.error?.message + 800 0 : if (type && message) return `${type}: ${message}` + 801 2 : return message || type || "Anthropic Messages stream error" + 802 : } + 803 : + 804 0 : const onError = (state: ParserState, event: AnthropicEvent): StepResult => [ + 805 0 : state, + 806 0 : [ + 807 0 : LLMEvent.providerError({ + 808 0 : message: providerErrorMessage(event), + 809 0 : classification: isContextOverflow(event.error?.message ?? "") ? "context-overflow" : undefined, + 810 0 : }), + 811 : ], + 812 2 : ] + 813 : + 814 0 : const step = (state: ParserState, event: AnthropicEvent) => { + 815 0 : if (event.type === "message_start") return Effect.succeed(onMessageStart(state, event)) + 816 0 : if (event.type === "content_block_start") return Effect.succeed(onContentBlockStart(state, event)) + 817 0 : if (event.type === "content_block_delta") return onContentBlockDelta(state, event) + 818 0 : if (event.type === "content_block_stop") return onContentBlockStop(state, event) + 819 0 : if (event.type === "message_delta") return Effect.succeed(onMessageDelta(state, event)) + 820 0 : if (event.type === "error") return Effect.succeed(onError(state, event)) + 821 2 : return Effect.succeed<StepResult>([state, NO_EVENTS]) + 822 : } + 823 : + 824 : // ============================================================================= + 825 : // Protocol And Anthropic Route + 826 : // ============================================================================= + 827 : /** + 828 : * The Anthropic Messages protocol — request body construction, body schema, + 829 : * and the streaming-event state machine. Used by native Anthropic Cloud and + 830 : * (once registered) Vertex Anthropic / Bedrock-hosted Anthropic passthrough. + 831 : */ + 832 40 : export const protocol = Protocol.make({ + 833 14 : id: ADAPTER, + 834 11 : body: { + 835 34 : schema: AnthropicMessagesBody, + 836 19 : from: fromRequest, + 837 4 : }, + 838 13 : stream: { + 839 46 : event: Protocol.jsonEvent(AnthropicEvent), + 840 13 : initial: () => ({ tools: ToolStream.empty<number>(), lifecycle: Lifecycle.initial() }), + 841 6 : step, + 842 2 : }, + 843 3 : }) + 844 : + 845 34 : export const route = Route.make({ + 846 14 : id: ADAPTER, + 847 24 : provider: "anthropic", + 848 11 : protocol, + 849 63 : endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }), + 850 18 : auth: Auth.none, + 851 23 : framing: Framing.sse, + 852 9 : headers: () => ({ "anthropic-version": "2023-06-01" }), + 853 3 : }) + 854 : + 855 57 : export * as AnthropicMessages from "./anthropic-messages" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| anthropic-messages.ts | +
+ |
+ 33.0 % | +660 | +218 | +|
| openai-chat.ts | +
+ |
+ 70.6 % | +384 | +271 | +|
| openai-compatible-chat.ts | +
+ |
+ 100.0 % | +12 | +12 | +|
| openai-responses.ts | +
+ |
+ 40.1 % | +793 | +318 | +|
| shared.ts | +
+ |
+ 40.1 % | +172 | +69 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| anthropic-messages.ts | +
+ |
+ 33.0 % | +660 | +218 | +|
| shared.ts | +
+ |
+ 40.1 % | +172 | +69 | +|
| openai-responses.ts | +
+ |
+ 40.1 % | +793 | +318 | +|
| openai-chat.ts | +
+ |
+ 70.6 % | +384 | +271 | +|
| openai-compatible-chat.ts | +
+ |
+ 100.0 % | +12 | +12 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| anthropic-messages.ts | +
+ |
+ 33.0 % | +660 | +218 | +|
| openai-chat.ts | +
+ |
+ 70.6 % | +384 | +271 | +|
| openai-compatible-chat.ts | +
+ |
+ 100.0 % | +12 | +12 | +|
| openai-responses.ts | +
+ |
+ 40.1 % | +793 | +318 | +|
| shared.ts | +
+ |
+ 40.1 % | +172 | +69 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 40 : import { Effect, Schema } from "effect" + 2 40 : import { Route } from "../route/client" + 3 37 : import { Auth } from "../route/auth" + 4 45 : import { Endpoint } from "../route/endpoint" + 5 51 : import { HttpTransport } from "../route/transport" + 6 45 : import { Protocol } from "../route/protocol" + 7 34 : import { + 8 : LLMEvent, + 9 : Usage, + 10 : type FinishReason, + 11 : type JsonSchema, + 12 : type LLMRequest, + 13 : type MediaPart, + 14 : type ReasoningPart, + 15 : type TextPart, + 16 : type ToolCallPart, + 17 : type ToolDefinition, + 18 : type ToolContent, + 19 : } from "../schema" + 20 93 : import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" + 21 55 : import { OpenAIOptions } from "./utils/openai-options" + 22 46 : import { Lifecycle } from "./utils/lifecycle" + 23 59 : import { ToolSchemaProjection } from "./utils/tool-schema" + 24 49 : import { ToolStream } from "./utils/tool-stream" + 25 : + 26 30 : const ADAPTER = "openai-chat" + 27 56 : const IMAGE_MIMES = new Set<string>(ProviderShared.IMAGE_MIMES) + 28 60 : export const DEFAULT_BASE_URL = "https://api.openai.com/v1" + 29 40 : export const PATH = "/chat/completions" + 30 : + 31 : // ============================================================================= + 32 : // Request Body Schema + 33 : // ============================================================================= + 34 : // The body schema is the provider-native JSON body. `fromRequest` below builds + 35 : // this shape from the common `LLMRequest`, then `Route.make` validates and + 36 : // JSON-encodes it before transport. + 37 43 : const OpenAIChatFunction = Schema.Struct({ + 38 22 : name: Schema.String, + 39 29 : description: Schema.String, + 40 23 : parameters: JsonObject, + 41 3 : }) + 42 : + 43 39 : const OpenAIChatTool = Schema.Struct({ + 44 31 : type: Schema.tag("function"), + 45 29 : function: OpenAIChatFunction, + 46 3 : }) + 47 : type OpenAIChatTool = Schema.Schema.Type<typeof OpenAIChatTool> + 48 : + 49 52 : const OpenAIChatAssistantToolCall = Schema.Struct({ + 50 20 : id: Schema.String, + 51 31 : type: Schema.tag("function"), + 52 29 : function: Schema.Struct({ + 53 24 : name: Schema.String, + 54 26 : arguments: Schema.String, + 55 3 : }), + 56 3 : }) + 57 : type OpenAIChatAssistantToolCall = Schema.Schema.Type<typeof OpenAIChatAssistantToolCall> + 58 : + 59 45 : const OpenAIChatUserContent = Schema.Union([ + 60 71 : Schema.Struct({ type: Schema.Literal("text"), text: Schema.String }), + 61 19 : Schema.Struct({ + 62 38 : type: Schema.Literal("image_url"), + 63 50 : image_url: Schema.Struct({ url: Schema.String }), + 64 3 : }), + 65 3 : ]) + 66 : + 67 41 : const OpenAIChatMessage = Schema.Union([ + 68 76 : Schema.Struct({ role: Schema.Literal("system"), content: Schema.String }), + 69 19 : Schema.Struct({ + 70 33 : role: Schema.Literal("user"), + 71 77 : content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), + 72 5 : }), + 73 19 : Schema.Struct({ + 74 38 : role: Schema.Literal("assistant"), + 75 42 : content: Schema.NullOr(Schema.String), + 76 59 : tool_calls: optionalArray(OpenAIChatAssistantToolCall), + 77 51 : reasoning_content: Schema.optional(Schema.String), + 78 5 : }), + 79 101 : Schema.Struct({ role: Schema.Literal("tool"), tool_call_id: Schema.String, content: Schema.String }), + 80 38 : ]).pipe(Schema.toTaggedUnion("role")) + 81 : type OpenAIChatMessage = Schema.Schema.Type<typeof OpenAIChatMessage> + 82 : + 83 44 : const OpenAIChatToolChoice = Schema.Union([ + 84 48 : Schema.Literals(["auto", "none", "required"]), + 85 19 : Schema.Struct({ + 86 33 : type: Schema.tag("function"), + 87 50 : function: Schema.Struct({ name: Schema.String }), + 88 3 : }), + 89 3 : ]) + 90 : + 91 28 : export const bodyFields = { + 92 23 : model: Schema.String, + 93 44 : messages: Schema.Array(OpenAIChatMessage), + 94 39 : tools: optionalArray(OpenAIChatTool), + 95 53 : tool_choice: Schema.optional(OpenAIChatToolChoice), + 96 31 : stream: Schema.Literal(true), + 97 84 : stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })), + 98 41 : store: Schema.optional(Schema.Boolean), + 99 73 : reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort), + 100 45 : max_tokens: Schema.optional(Schema.Number), + 101 46 : temperature: Schema.optional(Schema.Number), + 102 40 : top_p: Schema.optional(Schema.Number), + 103 52 : frequency_penalty: Schema.optional(Schema.Number), + 104 51 : presence_penalty: Schema.optional(Schema.Number), + 105 39 : seed: Schema.optional(Schema.Number), + 106 35 : stop: optionalArray(Schema.String), + 107 2 : } + 108 49 : const OpenAIChatBody = Schema.Struct(bodyFields) + 109 : export type OpenAIChatBody = Schema.Schema.Type<typeof OpenAIChatBody> + 110 : + 111 : // ============================================================================= + 112 : // Streaming Event Schema + 113 : // ============================================================================= + 114 : // The event schema is one decoded SSE `data:` payload. `Framing.sse` splits the + 115 : // byte stream into strings, then `Protocol.jsonEvent` decodes each string into + 116 : // this provider-native event shape. + 117 40 : const OpenAIChatUsage = Schema.Struct({ + 118 48 : prompt_tokens: Schema.optional(Schema.Number), + 119 52 : completion_tokens: Schema.optional(Schema.Number), + 120 47 : total_tokens: Schema.optional(Schema.Number), + 121 36 : prompt_tokens_details: optionalNull( + 122 19 : Schema.Struct({ + 123 47 : cached_tokens: Schema.optional(Schema.Number), + 124 2 : }), + 125 4 : ), + 126 40 : completion_tokens_details: optionalNull( + 127 19 : Schema.Struct({ + 128 50 : reasoning_tokens: Schema.optional(Schema.Number), + 129 2 : }), + 130 2 : ), + 131 3 : }) + 132 : + 133 56 : const OpenAIChatToolCallDeltaFunction = Schema.Struct({ + 134 36 : name: optionalNull(Schema.String), + 135 39 : arguments: optionalNull(Schema.String), + 136 3 : }) + 137 : + 138 48 : const OpenAIChatToolCallDelta = Schema.Struct({ + 139 23 : index: Schema.Number, + 140 34 : id: optionalNull(Schema.String), + 141 56 : function: optionalNull(OpenAIChatToolCallDeltaFunction), + 142 3 : }) + 143 : type OpenAIChatToolCallDelta = Schema.Schema.Type<typeof OpenAIChatToolCallDelta> + 144 : + 145 40 : const OpenAIChatDelta = Schema.Struct({ + 146 39 : content: optionalNull(Schema.String), + 147 49 : reasoning_content: optionalNull(Schema.String), + 148 64 : tool_calls: optionalNull(Schema.Array(OpenAIChatToolCallDelta)), + 149 3 : }) + 150 : + 151 41 : const OpenAIChatChoice = Schema.Struct({ + 152 39 : delta: optionalNull(OpenAIChatDelta), + 153 43 : finish_reason: optionalNull(Schema.String), + 154 3 : }) + 155 : + 156 40 : const OpenAIChatEvent = Schema.Struct({ + 157 42 : choices: Schema.Array(OpenAIChatChoice), + 158 37 : usage: optionalNull(OpenAIChatUsage), + 159 3 : }) + 160 : type OpenAIChatEvent = Schema.Schema.Type<typeof OpenAIChatEvent> + 161 : type OpenAIChatRequestMessage = LLMRequest["messages"][number] + 162 : + 163 : interface ParserState { + 164 : readonly tools: ToolStream.State<number> + 165 : readonly toolCallEvents: ReadonlyArray<LLMEvent> + 166 : readonly usage?: Usage + 167 : readonly finishReason?: FinishReason + 168 : readonly lifecycle: Lifecycle.State + 169 : } + 170 : + 171 46 : const invalid = ProviderShared.invalidRequest + 172 : + 173 : // ============================================================================= + 174 : // Request Lowering + 175 : // ============================================================================= + 176 : // Lowering is the only place that knows how common LLM messages map onto the + 177 : // OpenAI Chat wire format. Keep provider quirks here instead of leaking native + 178 : // fields into `LLMRequest`. + 179 0 : const lowerTool = (tool: ToolDefinition, inputSchema: JsonSchema): OpenAIChatTool => ({ + 180 0 : type: "function", + 181 0 : function: { + 182 0 : name: tool.name, + 183 0 : description: tool.description, + 184 0 : parameters: ToolSchemaProjection.openAI(inputSchema), + 185 : }, + 186 2 : }) + 187 : + 188 0 : const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) => + 189 0 : ProviderShared.matchToolChoice("OpenAI Chat", toolChoice, { + 190 0 : auto: () => "auto" as const, + 191 0 : none: () => "none" as const, + 192 0 : required: () => "required" as const, + 193 : tool: (name) => ({ type: "function" as const, function: { name } }), + 194 2 : }) + 195 : + 196 0 : const lowerToolCall = (part: ToolCallPart): OpenAIChatAssistantToolCall => ({ + 197 0 : id: part.id, + 198 0 : type: "function", + 199 0 : function: { + 200 0 : name: part.name, + 201 0 : arguments: ProviderShared.encodeJson(part.input), + 202 : }, + 203 2 : }) + 204 : + 205 0 : const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart) { + 206 0 : const media = yield* ProviderShared.validateMedia("OpenAI Chat", part, IMAGE_MIMES) + 207 : return { type: "image_url" as const, image_url: { url: media.dataUrl } } + 208 3 : }) + 209 : + 210 0 : const openAICompatibleReasoningContent = (native: unknown) => + 211 2 : isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined + 212 : + 213 76 : const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message: OpenAIChatRequestMessage) { + 214 21 : const content: Array<Schema.Schema.Type<typeof OpenAIChatUserContent>> = [] + 215 40 : for (const part of message.content) { + 216 32 : if (part.type === "text") { + 217 54 : content.push({ type: "text", text: part.text }) + 218 8 : continue + 219 0 : } + 220 0 : if (part.type === "media") { + 221 0 : content.push(yield* lowerMedia(part)) + 222 0 : continue + 223 0 : } + 224 0 : return yield* ProviderShared.unsupportedContent("OpenAI Chat", "user", ["text", "media"]) + 225 2 : } + 226 51 : if (content.every((part) => part.type === "text")) + 227 73 : return { role: "user" as const, content: content.map((part) => part.text).join("") } + 228 0 : return { role: "user" as const, content } + 229 3 : }) + 230 : + 231 0 : const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(function* ( + 232 0 : message: OpenAIChatRequestMessage, + 233 0 : ) { + 234 0 : const content: TextPart[] = [] + 235 0 : const reasoning: ReasoningPart[] = [] + 236 0 : const toolCalls: OpenAIChatAssistantToolCall[] = [] + 237 0 : for (const part of message.content) { + 238 0 : if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"])) + 239 0 : return yield* ProviderShared.unsupportedContent("OpenAI Chat", "assistant", ["text", "reasoning", "tool-call"]) + 240 0 : if (part.type === "text") { + 241 0 : content.push(part) + 242 0 : continue + 243 0 : } + 244 0 : if (part.type === "reasoning") { + 245 0 : reasoning.push(part) + 246 0 : continue + 247 0 : } + 248 0 : if (part.type === "tool-call") { + 249 0 : toolCalls.push(lowerToolCall(part)) + 250 0 : continue + 251 0 : } + 252 0 : } + 253 0 : return { + 254 0 : role: "assistant" as const, + 255 0 : content: content.length === 0 ? null : ProviderShared.joinText(content), + 256 0 : tool_calls: toolCalls.length === 0 ? undefined : toolCalls, + 257 0 : reasoning_content: + 258 0 : reasoning.length > 0 + 259 0 : ? reasoning.map((part) => part.text).join("") + 260 0 : : openAICompatibleReasoningContent(message.native?.openaiCompatible), + 261 : } + 262 3 : }) + 263 : + 264 0 : const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (message: OpenAIChatRequestMessage) { + 265 0 : const messages: OpenAIChatMessage[] = [] + 266 0 : const images: Array<Schema.Schema.Type<typeof OpenAIChatUserContent>> = [] + 267 0 : for (const part of message.content) { + 268 0 : if (!ProviderShared.supportsContent(part, ["tool-result"])) + 269 0 : return yield* ProviderShared.unsupportedContent("OpenAI Chat", "tool", ["tool-result"]) + 270 0 : if (part.result.type !== "content") { + 271 0 : messages.push({ role: "tool", tool_call_id: part.id, content: ProviderShared.toolResultText(part) }) + 272 0 : continue + 273 0 : } + 274 0 : const content: ReadonlyArray<ToolContent> = part.result.value + 275 0 : const text = content.filter((item) => item.type === "text").map((item) => item.text) + 276 0 : messages.push({ role: "tool", tool_call_id: part.id, content: text.join("\n") }) + 277 0 : const files = content.filter((item) => item.type === "file") + 278 0 : images.push( + 279 0 : ...(yield* Effect.forEach(files, (item) => + 280 0 : lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }), + 281 0 : )), + 282 0 : ) + 283 0 : } + 284 : return { messages, images } + 285 3 : }) + 286 : + 287 68 : const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message: OpenAIChatRequestMessage) { + 288 72 : if (message.role === "user") return [yield* lowerUserMessage(message)] + 289 0 : if (message.role === "assistant") return [yield* lowerAssistantMessage(message)] + 290 0 : return (yield* lowerToolMessages(message)).messages + 291 3 : }) + 292 : + 293 70 : const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: LLMRequest) { + 294 15 : const system: OpenAIChatMessage[] = + 295 33 : request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] + 296 31 : const messages = [...system] + 297 27 : const pendingImages: Array<Schema.Schema.Type<typeof OpenAIChatUserContent>> = [] + 298 30 : const flushImages = () => { + 299 42 : if (pendingImages.length === 0) return + 300 3 : messages.push({ role: "user", content: pendingImages.splice(0) }) + 301 : } + 302 44 : for (const message of request.messages) { + 303 30 : if (message.role === "system") { + 304 0 : const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Chat", message) + 305 0 : if (pendingImages.length > 0) { + 306 0 : messages.push({ role: "user", content: [...pendingImages.splice(0), { type: "text", text: part.text }] }) + 307 0 : continue + 308 0 : } + 309 0 : const previous = messages.at(-1) + 310 0 : if (previous?.role === "user" && typeof previous.content === "string") + 311 0 : messages[messages.length - 1] = { role: "user", content: `${previous.content}\n${part.text}` } + 312 0 : else if (previous?.role === "user" && Array.isArray(previous.content)) + 313 0 : messages[messages.length - 1] = { + 314 0 : role: "user", + 315 0 : content: [...previous.content, { type: "text", text: part.text }], + 316 0 : } + 317 0 : else messages.push({ role: "user", content: part.text }) + 318 0 : continue + 319 4 : } + 320 28 : if (message.role === "tool") { + 321 0 : const lowered = yield* lowerToolMessages(message) + 322 0 : messages.push(...lowered.messages) + 323 0 : pendingImages.push(...lowered.images) + 324 0 : continue + 325 4 : } + 326 18 : flushImages() + 327 49 : messages.push(...(yield* lowerMessage(message))) + 328 2 : } + 329 16 : flushImages() + 330 15 : return messages + 331 3 : }) + 332 : + 333 68 : const lowerOptions = Effect.fn("OpenAIChat.lowerOptions")(function* (request: LLMRequest) { + 334 45 : const store = OpenAIOptions.store(request) + 335 65 : const reasoningEffort = OpenAIOptions.reasoningEffort(request) + 336 76 : if (reasoningEffort && !OpenAIOptions.isReasoningEffort(reasoningEffort)) + 337 2 : return yield* invalid(`OpenAI Chat does not support reasoning effort ${reasoningEffort}`) + 338 15 : return { + 339 32 : ...(store !== undefined ? { store } : {}), + 340 22 : ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}), + 341 1 : } + 342 3 : }) + 343 : + 344 66 : const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (request: LLMRequest) { + 345 : // `fromRequest` returns the provider body only. Endpoint, auth, framing, + 346 : // validation, and HTTP execution are composed by `Route.make`. + 347 40 : const generation = request.generation + 348 74 : const toolSchemaCompatibility = request.model.compatibility?.toolSchema + 349 12 : return { + 350 28 : model: request.model.id, + 351 44 : messages: yield* lowerMessages(request), + 352 7 : tools: + 353 28 : request.tools.length === 0 + 354 9 : ? undefined + 355 0 : : request.tools.map((tool) => + 356 : lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility)), + 357 4 : ), + 358 48 : tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined, + 359 17 : stream: true as const, + 360 44 : stream_options: { include_usage: true }, + 361 38 : max_tokens: generation?.maxTokens, + 362 41 : temperature: generation?.temperature, + 363 28 : top_p: generation?.topP, + 364 52 : frequency_penalty: generation?.frequencyPenalty, + 365 50 : presence_penalty: generation?.presencePenalty, + 366 27 : seed: generation?.seed, + 367 30 : stop: generation?.stop, + 368 30 : ...(yield* lowerOptions(request)), + 369 1 : } + 370 3 : }) + 371 : + 372 : // ============================================================================= + 373 : // Stream Parsing + 374 : // ============================================================================= + 375 : // Streaming parsers are small state machines: every event returns a new state + 376 : // plus the common `LLMEvent`s produced by that event. Tool calls are accumulated + 377 : // because OpenAI streams JSON arguments across multiple deltas. + 378 37 : const mapFinishReason = (reason: string | null | undefined): FinishReason => { + 379 38 : if (reason === "stop") return "stop" + 380 0 : if (reason === "length") return "length" + 381 0 : if (reason === "content_filter") return "content-filter" + 382 0 : if (reason === "function_call" || reason === "tool_calls") return "tool-calls" + 383 2 : return "unknown" + 384 : } + 385 : + 386 : // OpenAI Chat reports `prompt_tokens` (inclusive total) with a + 387 : // `cached_tokens` subset, and `completion_tokens` (inclusive total) with + 388 : // a `reasoning_tokens` subset. We pass the inclusive totals through and + 389 : // derive the non-cached breakdown so the `LLM.Usage` contract is + 390 : // satisfied on both sides. + 391 29 : const mapUsage = (usage: OpenAIChatEvent["usage"]): Usage | undefined => { + 392 22 : if (!usage) return undefined + 393 60 : const cached = usage.prompt_tokens_details?.cached_tokens + 394 70 : const reasoning = usage.completion_tokens_details?.reasoning_tokens + 395 79 : const nonCached = ProviderShared.subtractTokens(usage.prompt_tokens, cached) + 396 22 : return new Usage({ + 397 37 : inputTokens: usage.prompt_tokens, + 398 42 : outputTokens: usage.completion_tokens, + 399 36 : nonCachedInputTokens: nonCached, + 400 33 : cacheReadInputTokens: cached, + 401 31 : reasoningTokens: reasoning, + 402 110 : totalTokens: ProviderShared.totalTokens(usage.prompt_tokens, usage.completion_tokens, usage.total_tokens), + 403 37 : providerMetadata: { openai: usage }, + 404 4 : }) + 405 : } + 406 : + 407 29 : const step = (state: ParserState, event: OpenAIChatEvent) => + 408 15 : Effect.gen(function* () { + 409 20 : const events: LLMEvent[] = [] + 410 53 : const usage = mapUsage(event.usage) ?? state.usage + 411 34 : const choice = event.choices[0] + 412 103 : const finishReason = choice?.finish_reason ? mapFinishReason(choice.finish_reason) : state.finishReason + 413 30 : const delta = choice?.delta + 414 45 : const toolDeltas = delta?.tool_calls ?? [] + 415 26 : let tools = state.tools + 416 : + 417 34 : let lifecycle = state.lifecycle + 418 : + 419 32 : if (delta?.reasoning_content) + 420 2 : lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", delta.reasoning_content) + 421 : + 422 24 : if (delta?.content) { + 423 73 : lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0") + 424 78 : lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content) + 425 2 : } + 426 : + 427 27 : if (toolDeltas.length) lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0") + 428 : + 429 30 : for (const tool of toolDeltas) { + 430 0 : const result = ToolStream.appendOrStart( + 431 0 : ADAPTER, + 432 0 : tools, + 433 0 : tool.index, + 434 0 : { id: tool.id ?? undefined, name: tool.function?.name ?? undefined, text: tool.function?.arguments ?? "" }, + 435 0 : "OpenAI Chat tool call delta is missing id or name", + 436 0 : ) + 437 0 : if (ToolStream.isError(result)) return yield* result + 438 0 : tools = result.tools + 439 0 : if (result.events.length) lifecycle = Lifecycle.stepStart(lifecycle, events) + 440 0 : events.push(...result.events) + 441 2 : } + 442 : + 443 : // Finalize accumulated tool inputs eagerly when finish_reason arrives so + 444 : // JSON parse failures fail the stream at the boundary rather than at halt. + 445 17 : const finished = + 446 97 : finishReason !== undefined && state.finishReason === undefined && Object.keys(tools).length > 0 + 447 2 : ? yield* ToolStream.finishAll(ADAPTER, tools) + 448 11 : : undefined + 449 : + 450 12 : return [ + 451 7 : { + 452 38 : tools: finished?.tools ?? tools, + 453 63 : toolCallEvents: finished?.events ?? state.toolCallEvents, + 454 12 : usage, + 455 19 : finishReason, + 456 13 : lifecycle, + 457 6 : }, + 458 8 : events, + 459 1 : ] as const + 460 2 : }) + 461 : + 462 33 : const finishEvents = (state: ParserState): ReadonlyArray<LLMEvent> => { + 463 20 : const events: LLMEvent[] = [] + 464 55 : const hasToolCalls = state.toolCallEvents.length > 0 + 465 84 : const reason = state.finishReason === "stop" && hasToolCalls ? "tool-calls" : state.finishReason + 466 66 : const lifecycle = state.toolCallEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle + 467 39 : events.push(...state.toolCallEvents) + 468 83 : if (reason) Lifecycle.finish(lifecycle, events, { reason, usage: state.usage }) + 469 15 : return events + 470 : } + 471 : + 472 : // ============================================================================= + 473 : // Protocol And OpenAI Route + 474 : // ============================================================================= + 475 : /** + 476 : * The OpenAI Chat protocol — request body construction, body schema, and the + 477 : * streaming-event state machine. Reused by every route that speaks OpenAI Chat + 478 : * over HTTP+SSE: native OpenAI, DeepSeek, TogetherAI, Cerebras, Baseten, + 479 : * Fireworks, DeepInfra, and (once added) Azure OpenAI Chat. + 480 : */ + 481 40 : export const protocol = Protocol.make({ + 482 14 : id: ADAPTER, + 483 11 : body: { + 484 27 : schema: OpenAIChatBody, + 485 19 : from: fromRequest, + 486 4 : }, + 487 13 : stream: { + 488 47 : event: Protocol.jsonEvent(OpenAIChatEvent), + 489 101 : initial: () => ({ tools: ToolStream.empty<number>(), toolCallEvents: [], lifecycle: Lifecycle.initial() }), + 490 9 : step, + 491 22 : onHalt: finishEvents, + 492 2 : }, + 493 3 : }) + 494 : + 495 58 : export const httpTransport = HttpTransport.sseJson.with<OpenAIChatBody>() + 496 : + 497 34 : export const route = Route.make({ + 498 14 : id: ADAPTER, + 499 21 : provider: "openai", + 500 11 : protocol, + 501 63 : endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }), + 502 18 : auth: Auth.none, + 503 25 : transport: httpTransport, + 504 3 : }) + 505 : + 506 43 : export * as OpenAIChat from "./openai-chat" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 40 : import { Route, type RouteRoutedModelInput } from "../route/client" + 2 45 : import { Endpoint } from "../route/endpoint" + 3 43 : import { Framing } from "../route/framing" + 4 44 : import * as OpenAIChat from "./openai-chat" + 5 : + 6 41 : const ADAPTER = "openai-compatible-chat" + 7 : + 8 : export type OpenAICompatibleChatModelInput = RouteRoutedModelInput + 9 : + 10 : /** + 11 : * Route for non-OpenAI providers that expose an OpenAI Chat-compatible + 12 : * `/chat/completions` endpoint. Reuses `OpenAIChat.protocol` end-to-end and + 13 : * overrides only the route id so providers can be resolved per-family without + 14 : * colliding with native OpenAI. Provider helpers configure the route endpoint + 15 : * before model selection. + 16 : */ + 17 34 : export const route = Route.make({ + 18 14 : id: ADAPTER, + 19 32 : protocol: OpenAIChat.protocol, + 20 47 : endpoint: Endpoint.path("/chat/completions"), + 21 21 : framing: Framing.sse, + 22 3 : }) + 23 : + 24 64 : export * as OpenAICompatibleChat from "./openai-compatible-chat" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 40 : import { Effect, Schema } from "effect" + 2 40 : import { Route } from "../route/client" + 3 37 : import { Auth } from "../route/auth" + 4 45 : import { Endpoint } from "../route/endpoint" + 5 71 : import { HttpTransport, WebSocketTransport } from "../route/transport" + 6 45 : import { Protocol } from "../route/protocol" + 7 45 : import { + 8 : LLMEvent, + 9 : Usage, + 10 : type FinishReason, + 11 : type JsonSchema, + 12 : type LLMRequest, + 13 : type ProviderMetadata, + 14 : type ReasoningPart, + 15 : type TextPart, + 16 : type ToolCallPart, + 17 : type ToolDefinition, + 18 : type ToolContent, + 19 : type ToolResultPart, + 20 : } from "../schema" + 21 83 : import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" + 22 54 : import { isContextOverflow } from "../provider-error" + 23 55 : import { OpenAIOptions } from "./utils/openai-options" + 24 46 : import { Lifecycle } from "./utils/lifecycle" + 25 59 : import { ToolSchemaProjection } from "./utils/tool-schema" + 26 49 : import { ToolStream } from "./utils/tool-stream" + 27 : + 28 35 : const ADAPTER = "openai-responses" + 29 60 : export const DEFAULT_BASE_URL = "https://api.openai.com/v1" + 30 33 : export const PATH = "/responses" + 31 : + 32 : // ============================================================================= + 33 : // Request Body Schema + 34 : // ============================================================================= + 35 49 : const OpenAIResponsesInputText = Schema.Struct({ + 36 33 : type: Schema.tag("input_text"), + 37 20 : text: Schema.String, + 38 3 : }) + 39 50 : const OpenAIResponsesInputImage = Schema.Struct({ + 40 34 : type: Schema.tag("input_image"), + 41 25 : image_url: Schema.String, + 42 3 : }) + 43 104 : const OpenAIResponsesInputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage]) + 44 : type OpenAIResponsesInputContent = Schema.Schema.Type<typeof OpenAIResponsesInputContent> + 45 : + 46 50 : const OpenAIResponsesOutputText = Schema.Struct({ + 47 34 : type: Schema.tag("output_text"), + 48 20 : text: Schema.String, + 49 3 : }) + 50 : + 51 60 : const OpenAIResponsesReasoningSummaryText = Schema.Struct({ + 52 35 : type: Schema.tag("summary_text"), + 53 20 : text: Schema.String, + 54 3 : }) + 55 : + 56 53 : const OpenAIResponsesReasoningItem = Schema.Struct({ + 57 32 : type: Schema.tag("reasoning"), + 58 40 : id: Schema.optionalKey(Schema.String), + 59 61 : summary: Schema.Array(OpenAIResponsesReasoningSummaryText), + 60 47 : encrypted_content: optionalNull(Schema.String), + 61 3 : }) + 62 : + 63 53 : const OpenAIResponsesItemReference = Schema.Struct({ + 64 37 : type: Schema.tag("item_reference"), + 65 18 : id: Schema.String, + 66 3 : }) + 67 : + 68 : // `function_call_output.output` accepts either a plain string or an ordered + 69 : // array of content items so tools can return images in addition to text. + 70 : // https://platform.openai.com/docs/api-reference/responses/object + 71 117 : const OpenAIResponsesFunctionCallOutputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage]) + 72 : + 73 57 : const OpenAIResponsesFunctionCallOutput = Schema.Union([ + 74 16 : Schema.String, + 75 55 : Schema.Array(OpenAIResponsesFunctionCallOutputContent), + 76 3 : ]) + 77 : + 78 48 : const OpenAIResponsesInputItem = Schema.Union([ + 79 72 : Schema.Struct({ role: Schema.tag("system"), content: Schema.String }), + 80 98 : Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }), + 81 101 : Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }), + 82 31 : OpenAIResponsesReasoningItem, + 83 31 : OpenAIResponsesItemReference, + 84 19 : Schema.Struct({ + 85 38 : type: Schema.tag("function_call"), + 86 27 : call_id: Schema.String, + 87 24 : name: Schema.String, + 88 26 : arguments: Schema.String, + 89 5 : }), + 90 19 : Schema.Struct({ + 91 45 : type: Schema.tag("function_call_output"), + 92 27 : call_id: Schema.String, + 93 43 : output: OpenAIResponsesFunctionCallOutput, + 94 3 : }), + 95 3 : ]) + 96 : type OpenAIResponsesInputItem = Schema.Schema.Type<typeof OpenAIResponsesInputItem> + 97 : + 98 : // Mutable counterpart of the schema reasoning item so `lowerMessages` can fold + 99 : // multiple streamed summary parts into the same item before flushing. + 100 : type OpenAIResponsesReasoningInput = { + 101 : type: "reasoning" + 102 : id: string + 103 : summary: Array<{ type: "summary_text"; text: string }> + 104 : encrypted_content?: string | null + 105 : } + 106 : type OpenAIResponsesReasoningReplay = Omit<OpenAIResponsesReasoningInput, "id"> + 107 : + 108 44 : const OpenAIResponsesTool = Schema.Struct({ + 109 31 : type: Schema.tag("function"), + 110 22 : name: Schema.String, + 111 29 : description: Schema.String, + 112 25 : parameters: JsonObject, + 113 40 : strict: Schema.optional(Schema.Boolean), + 114 3 : }) + 115 : type OpenAIResponsesTool = Schema.Schema.Type<typeof OpenAIResponsesTool> + 116 : + 117 49 : const OpenAIResponsesToolChoice = Schema.Union([ + 118 48 : Schema.Literals(["auto", "none", "required"]), + 119 69 : Schema.Struct({ type: Schema.tag("function"), name: Schema.String }), + 120 3 : ]) + 121 : + 122 : // Fields shared between the HTTP body and the WebSocket `response.create` + 123 : // message. The HTTP body adds `stream: true`; the WebSocket message adds + 124 : // `type: "response.create"`. Defining the shared shape once keeps the two + 125 : // transports in sync without a destructure-and-strip dance. + 126 36 : const OpenAIResponsesCoreFields = { + 127 23 : model: Schema.String, + 128 48 : input: Schema.Array(OpenAIResponsesInputItem), + 129 47 : instructions: Schema.optional(Schema.String), + 130 44 : tools: optionalArray(OpenAIResponsesTool), + 131 58 : tool_choice: Schema.optional(OpenAIResponsesToolChoice), + 132 41 : store: Schema.optional(Schema.Boolean), + 133 65 : service_tier: Schema.optional(OpenAIOptions.OpenAIServiceTier), + 134 51 : prompt_cache_key: Schema.optional(Schema.String), + 135 65 : include: optionalArray(OpenAIOptions.OpenAIResponseIncludable), + 136 27 : reasoning: Schema.optional( + 137 19 : Schema.Struct({ + 138 65 : effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort), + 139 50 : summary: Schema.optional(Schema.Literal("auto")), + 140 2 : }), + 141 4 : ), + 142 22 : text: Schema.optional( + 143 19 : Schema.Struct({ + 144 63 : verbosity: Schema.optional(OpenAIOptions.OpenAITextVerbosity), + 145 2 : }), + 146 4 : ), + 147 52 : max_output_tokens: Schema.optional(Schema.Number), + 148 46 : temperature: Schema.optional(Schema.Number), + 149 38 : top_p: Schema.optional(Schema.Number), + 150 2 : } + 151 : + 152 47 : const OpenAIResponsesBody = Schema.Struct({ + 153 28 : ...OpenAIResponsesCoreFields, + 154 29 : stream: Schema.Literal(true), + 155 3 : }) + 156 : export type OpenAIResponsesBody = Schema.Schema.Type<typeof OpenAIResponsesBody> + 157 : + 158 61 : const OpenAIResponsesWebSocketMessage = Schema.StructWithRest( + 159 17 : Schema.Struct({ + 160 41 : type: Schema.tag("response.create"), + 161 26 : ...OpenAIResponsesCoreFields, + 162 3 : }), + 163 46 : [Schema.Record(Schema.String, Schema.Unknown)], + 164 3 : ) + 165 : type OpenAIResponsesWebSocketMessage = Schema.Schema.Type<typeof OpenAIResponsesWebSocketMessage> + 166 105 : const encodeWebSocketMessage = Schema.encodeSync(Schema.fromJsonString(OpenAIResponsesWebSocketMessage)) + 167 : + 168 45 : const OpenAIResponsesUsage = Schema.Struct({ + 169 47 : input_tokens: Schema.optional(Schema.Number), + 170 103 : input_tokens_details: optionalNull(Schema.Struct({ cached_tokens: Schema.optional(Schema.Number) })), + 171 48 : output_tokens: Schema.optional(Schema.Number), + 172 107 : output_tokens_details: optionalNull(Schema.Struct({ reasoning_tokens: Schema.optional(Schema.Number) })), + 173 45 : total_tokens: Schema.optional(Schema.Number), + 174 3 : }) + 175 : type OpenAIResponsesUsage = Schema.Schema.Type<typeof OpenAIResponsesUsage> + 176 : + 177 50 : const OpenAIResponsesStreamItem = Schema.Struct({ + 178 22 : type: Schema.String, + 179 37 : id: Schema.optional(Schema.String), + 180 42 : call_id: Schema.optional(Schema.String), + 181 39 : name: Schema.optional(Schema.String), + 182 44 : arguments: Schema.optional(Schema.String), + 183 : // Hosted (provider-executed) tool fields. Each hosted tool item carries its + 184 : // own subset of these — we capture them generically so we can surface the + 185 : // call's typed input portion and round-trip the full result payload without + 186 : // hand-rolling a per-tool schema. + 187 41 : status: Schema.optional(Schema.String), + 188 42 : action: Schema.optional(Schema.Unknown), + 189 43 : queries: Schema.optional(Schema.Unknown), + 190 43 : results: Schema.optional(Schema.Unknown), + 191 39 : code: Schema.optional(Schema.String), + 192 47 : container_id: Schema.optional(Schema.String), + 193 43 : outputs: Schema.optional(Schema.Unknown), + 194 47 : server_label: Schema.optional(Schema.String), + 195 42 : output: Schema.optional(Schema.Unknown), + 196 41 : error: Schema.optional(Schema.Unknown), + 197 47 : encrypted_content: optionalNull(Schema.String), + 198 3 : }) + 199 : type OpenAIResponsesStreamItem = Schema.Schema.Type<typeof OpenAIResponsesStreamItem> + 200 : + 201 : // OpenAI Responses surfaces provider failures in two related shapes. The + 202 : // streaming `error` event carries the details at the top level + 203 : // (`{ type: "error", code, message, param, sequence_number }`), while + 204 : // `response.failed` carries them under `response.error`. We capture both so + 205 : // the parser can surface a useful provider-error message in either path. + 206 52 : const OpenAIResponsesErrorPayload = Schema.Struct({ + 207 36 : code: optionalNull(Schema.String), + 208 39 : message: optionalNull(Schema.String), + 209 35 : param: optionalNull(Schema.String), + 210 3 : }) + 211 : + 212 45 : const OpenAIResponsesEvent = Schema.Struct({ + 213 22 : type: Schema.String, + 214 40 : delta: Schema.optional(Schema.String), + 215 42 : item_id: Schema.optional(Schema.String), + 216 48 : summary_index: Schema.optional(Schema.Number), + 217 51 : item: Schema.optional(OpenAIResponsesStreamItem), + 218 26 : response: Schema.optional( + 219 22 : Schema.StructWithRest( + 220 19 : Schema.Struct({ + 221 39 : id: Schema.optional(Schema.String), + 222 46 : service_tier: optionalNull(Schema.String), + 223 79 : incomplete_details: optionalNull(Schema.Struct({ reason: Schema.String })), + 224 46 : usage: optionalNull(OpenAIResponsesUsage), + 225 50 : error: optionalNull(OpenAIResponsesErrorPayload), + 226 4 : }), + 227 46 : [Schema.Record(Schema.String, Schema.Unknown)], + 228 1 : ), + 229 4 : ), + 230 39 : code: Schema.optional(Schema.String), + 231 42 : message: Schema.optional(Schema.String), + 232 38 : param: Schema.optional(Schema.String), + 233 3 : }) + 234 : type OpenAIResponsesEvent = Schema.Schema.Type<typeof OpenAIResponsesEvent> + 235 : + 236 : interface ParserState { + 237 : readonly tools: ToolStream.State<string> + 238 : readonly hasFunctionCall: boolean + 239 : readonly lifecycle: Lifecycle.State + 240 : readonly reasoningItems: Readonly<Record<string, ReasoningStreamItem>> + 241 : readonly store: boolean | undefined + 242 : } + 243 : + 244 : type ReasoningSummaryStatus = "active" | "can-conclude" | "concluded" + 245 : + 246 : interface ReasoningStreamItem { + 247 : readonly encryptedContent: string | null | undefined + 248 : // Keyed by OpenAI's numeric `summary_index`. JS object keys coerce to + 249 : // strings, but typing the map as `Record<number, ...>` documents intent + 250 : // and matches the wire field. + 251 : readonly summaryParts: Readonly<Record<number, ReasoningSummaryStatus>> + 252 : } + 253 : + 254 46 : const invalid = ProviderShared.invalidRequest + 255 : + 256 : // ============================================================================= + 257 : // Request Lowering + 258 : // ============================================================================= + 259 0 : const lowerTool = (tool: ToolDefinition, inputSchema: JsonSchema): OpenAIResponsesTool => ({ + 260 0 : type: "function", + 261 0 : name: tool.name, + 262 0 : description: tool.description, + 263 0 : parameters: ToolSchemaProjection.openAI(inputSchema), + 264 0 : // TODO: Read this from OpenAI-specific tool options so direct LLM callers can opt into strict schemas. + 265 : strict: false, + 266 2 : }) + 267 : + 268 0 : const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) => + 269 0 : ProviderShared.matchToolChoice("OpenAI Responses", toolChoice, { + 270 0 : auto: () => "auto" as const, + 271 0 : none: () => "none" as const, + 272 0 : required: () => "required" as const, + 273 : tool: (name) => ({ type: "function" as const, name }), + 274 2 : }) + 275 : + 276 0 : const lowerToolCall = (part: ToolCallPart): OpenAIResponsesInputItem => ({ + 277 0 : type: "function_call", + 278 0 : call_id: part.id, + 279 0 : name: part.name, + 280 : arguments: ProviderShared.encodeJson(part.input), + 281 2 : }) + 282 : + 283 0 : const lowerReasoning = (part: ReasoningPart): OpenAIResponsesReasoningInput | undefined => { + 284 0 : const openai = part.providerMetadata?.openai + 285 0 : if (!ProviderShared.isRecord(openai) || typeof openai.itemId !== "string" || openai.itemId.length === 0) + 286 0 : return undefined + 287 0 : const encryptedContent = + 288 0 : typeof openai.reasoningEncryptedContent === "string" + 289 0 : ? openai.reasoningEncryptedContent + 290 0 : : openai.reasoningEncryptedContent === null + 291 0 : ? null + 292 0 : : undefined + 293 0 : return { + 294 0 : type: "reasoning", + 295 0 : id: openai.itemId, + 296 0 : summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [], + 297 0 : encrypted_content: encryptedContent, + 298 2 : } + 299 : } + 300 : + 301 0 : const hostedToolItemID = (part: ToolResultPart) => { + 302 0 : const openai = part.providerMetadata?.openai + 303 0 : return ProviderShared.isRecord(openai) && typeof openai.itemId === "string" && openai.itemId.length > 0 + 304 0 : ? openai.itemId + 305 2 : : undefined + 306 : } + 307 : + 308 70 : const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* ( + 309 5 : part: LLMRequest["messages"][number]["content"][number], + 310 3 : ) { + 311 74 : if (part.type === "text") return { type: "input_text" as const, text: part.text } + 312 0 : if (part.type === "media") { + 313 0 : const media = yield* ProviderShared.validateMedia( + 314 0 : "OpenAI Responses", + 315 0 : part, + 316 0 : new Set<string>(ProviderShared.IMAGE_MIMES), + 317 0 : ) + 318 0 : return { type: "input_image" as const, image_url: media.dataUrl } + 319 0 : } + 320 0 : return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text", "media"]) + 321 3 : }) + 322 : + 323 : // Tool results may carry structured text/images. Keep media as provider-native + 324 : // content instead of JSON-stringifying base64 into a prompt string. + 325 0 : const lowerToolResultContentItem = Effect.fn("OpenAIResponses.lowerToolResultContentItem")(function* ( + 326 0 : item: ToolContent, + 327 0 : ) { + 328 0 : if (item.type === "text") return { type: "input_text" as const, text: item.text } + 329 0 : const media = yield* ProviderShared.validateToolFile( + 330 0 : "OpenAI Responses", + 331 0 : item, + 332 0 : new Set<string>(ProviderShared.IMAGE_MIMES), + 333 0 : ) + 334 : return { type: "input_image" as const, image_url: media.dataUrl } + 335 3 : }) + 336 : + 337 0 : const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput")(function* (part: ToolResultPart) { + 338 0 : // Text/json/error results are encoded as a plain string for backward + 339 0 : // compatibility with existing cassettes and provider expectations. + 340 0 : if (part.result.type !== "content") return ProviderShared.toolResultText(part) + 341 0 : // Preserve the narrowed array element type when compiled through a consumer package. + 342 0 : const content: ReadonlyArray<ToolContent> = part.result.value + 343 : return yield* Effect.forEach(content, lowerToolResultContentItem) + 344 3 : }) + 345 : + 346 75 : const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) { + 347 15 : const system: OpenAIResponsesInputItem[] = + 348 33 : request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] + 349 28 : const input: OpenAIResponsesInputItem[] = [...system] + 350 45 : const store = OpenAIOptions.store(request) + 351 : + 352 44 : for (const message of request.messages) { + 353 30 : if (message.role === "system") { + 354 0 : const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Responses", message) + 355 0 : const previous = input.at(-1) + 356 0 : if (previous && "role" in previous && previous.role === "user") + 357 0 : input[input.length - 1] = { + 358 0 : role: "user", + 359 0 : content: [...previous.content, { type: "input_text", text: part.text }], + 360 0 : } + 361 0 : else input.push({ role: "user", content: [{ type: "input_text", text: part.text }] }) + 362 0 : continue + 363 4 : } + 364 : + 365 35 : if (message.role === "user") { + 366 102 : input.push({ role: "user", content: yield* Effect.forEach(message.content, lowerUserContent) }) + 367 8 : continue + 368 0 : } + 369 : + 370 0 : if (message.role === "assistant") { + 371 0 : const content: TextPart[] = [] + 372 0 : const reasoningItems: Record<string, OpenAIResponsesReasoningReplay> = {} + 373 0 : const reasoningReferences = new Set<string>() + 374 0 : const hostedToolReferences = new Set<string>() + 375 0 : const flushText = () => { + 376 0 : if (content.length === 0) return + 377 0 : input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) }) + 378 0 : content.splice(0, content.length) + 379 : } + 380 0 : for (const part of message.content) { + 381 0 : if (part.type === "text") { + 382 0 : content.push(part) + 383 0 : continue + 384 0 : } + 385 0 : if (part.type === "reasoning") { + 386 0 : flushText() + 387 0 : const reasoning = lowerReasoning(part) + 388 0 : if (!reasoning) continue + 389 0 : if (store !== false) { + 390 0 : if (!reasoningReferences.has(reasoning.id)) input.push({ type: "item_reference", id: reasoning.id }) + 391 0 : reasoningReferences.add(reasoning.id) + 392 0 : continue + 393 0 : } + 394 0 : const existing = reasoningItems[reasoning.id] + 395 0 : if (existing) { + 396 0 : existing.summary.push(...reasoning.summary) + 397 0 : if (typeof reasoning.encrypted_content === "string") + 398 0 : existing.encrypted_content = reasoning.encrypted_content + 399 0 : continue + 400 0 : } + 401 0 : const replay = { + 402 0 : type: reasoning.type, + 403 0 : summary: reasoning.summary, + 404 0 : encrypted_content: reasoning.encrypted_content, + 405 0 : } + 406 0 : reasoningItems[reasoning.id] = replay + 407 0 : input.push(replay) + 408 0 : continue + 409 0 : } + 410 0 : if (part.type === "tool-call") { + 411 0 : flushText() + 412 0 : if (part.providerExecuted === true) continue + 413 0 : input.push(lowerToolCall(part)) + 414 0 : continue + 415 0 : } + 416 0 : if (part.type === "tool-result" && part.providerExecuted === true) { + 417 0 : flushText() + 418 0 : const itemID = hostedToolItemID(part) + 419 0 : if (store !== false && itemID && !hostedToolReferences.has(itemID)) + 420 0 : input.push({ type: "item_reference", id: itemID }) + 421 0 : if (itemID) hostedToolReferences.add(itemID) + 422 0 : continue + 423 0 : } + 424 0 : return yield* ProviderShared.unsupportedContent("OpenAI Responses", "assistant", [ + 425 0 : "text", + 426 0 : "reasoning", + 427 0 : "tool-call", + 428 0 : "tool-result", + 429 0 : ]) + 430 0 : } + 431 0 : flushText() + 432 0 : continue + 433 0 : } + 434 : + 435 0 : for (const part of message.content) { + 436 0 : if (!ProviderShared.supportsContent(part, ["tool-result"])) + 437 0 : return yield* ProviderShared.unsupportedContent("OpenAI Responses", "tool", ["tool-result"]) + 438 0 : input.push({ + 439 0 : type: "function_call_output", + 440 0 : call_id: part.id, + 441 0 : output: yield* lowerToolResultOutput(part), + 442 0 : }) + 443 0 : } + 444 2 : } + 445 : + 446 : // With store:false, OpenAI only accepts previous reasoning items when the + 447 : // complete item has encrypted state. Summary blocks for one item may carry + 448 : // that state only on the last block, so filter after they have been joined. + 449 24 : return store === false + 450 12 : ? input.filter( + 451 101 : (item) => !("type" in item) || item.type !== "reasoning" || typeof item.encrypted_content === "string", + 452 1 : ) + 453 0 : : input + 454 3 : }) + 455 : + 456 73 : const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (request: LLMRequest) { + 457 45 : const store = OpenAIOptions.store(request) + 458 63 : const promptCacheKey = OpenAIOptions.promptCacheKey(request) + 459 56 : const effort = OpenAIOptions.reasoningEffort(request) + 460 58 : if (effort && !OpenAIOptions.isReasoningEffort(effort)) + 461 2 : return yield* invalid(`OpenAI Responses does not support reasoning effort ${effort}`) + 462 58 : const summary = OpenAIOptions.reasoningSummary(request) + 463 49 : const include = OpenAIOptions.include(request) + 464 57 : const verbosity = OpenAIOptions.textVerbosity(request) + 465 59 : const instructions = OpenAIOptions.instructions(request) + 466 57 : const serviceTier = OpenAIOptions.serviceTier(request) + 467 15 : return { + 468 25 : ...(instructions ? { instructions } : {}), + 469 37 : ...(store !== undefined ? { store } : {}), + 470 27 : ...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}), + 471 20 : ...(include ? { include } : {}), + 472 30 : ...(effort || summary ? { reasoning: { effort, summary } } : {}), + 473 22 : ...(verbosity ? { text: { verbosity } } : {}), + 474 18 : ...(serviceTier ? { service_tier: serviceTier } : {}), + 475 1 : } + 476 3 : }) + 477 : + 478 71 : const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: LLMRequest) { + 479 40 : const generation = request.generation + 480 47 : const options = yield* lowerOptions(request) + 481 74 : const toolSchemaCompatibility = request.model.compatibility?.toolSchema + 482 12 : return { + 483 28 : model: request.model.id, + 484 41 : input: yield* lowerMessages(request), + 485 7 : tools: + 486 28 : request.tools.length === 0 + 487 9 : ? undefined + 488 0 : : request.tools.map((tool) => + 489 : lowerTool(tool, ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility)), + 490 4 : ), + 491 48 : tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined, + 492 17 : stream: true as const, + 493 45 : max_output_tokens: generation?.maxTokens, + 494 41 : temperature: generation?.temperature, + 495 31 : top_p: generation?.topP, + 496 9 : ...options, + 497 1 : } + 498 3 : }) + 499 : + 500 : // ============================================================================= + 501 : // Stream Parsing + 502 : // ============================================================================= + 503 : // OpenAI Responses reports `input_tokens` (inclusive total) with a + 504 : // `cached_tokens` subset, and `output_tokens` (inclusive total) with a + 505 : // `reasoning_tokens` subset. Pass the totals through and derive the + 506 : // non-cached breakdown. + 507 0 : const mapUsage = (usage: OpenAIResponsesUsage | null | undefined) => { + 508 0 : if (!usage) return undefined + 509 0 : const cached = usage.input_tokens_details?.cached_tokens + 510 0 : const reasoning = usage.output_tokens_details?.reasoning_tokens + 511 0 : const nonCached = ProviderShared.subtractTokens(usage.input_tokens, cached) + 512 0 : return new Usage({ + 513 0 : inputTokens: usage.input_tokens, + 514 0 : outputTokens: usage.output_tokens, + 515 0 : nonCachedInputTokens: nonCached, + 516 0 : cacheReadInputTokens: cached, + 517 0 : reasoningTokens: reasoning, + 518 0 : totalTokens: ProviderShared.totalTokens(usage.input_tokens, usage.output_tokens, usage.total_tokens), + 519 0 : providerMetadata: { openai: usage }, + 520 2 : }) + 521 : } + 522 : + 523 0 : const mapFinishReason = (event: OpenAIResponsesEvent, hasFunctionCall: boolean): FinishReason => { + 524 0 : const reason = event.response?.incomplete_details?.reason + 525 0 : if (reason === undefined || reason === null) return hasFunctionCall ? "tool-calls" : "stop" + 526 0 : if (reason === "max_output_tokens") return "length" + 527 0 : if (reason === "content_filter") return "content-filter" + 528 2 : return hasFunctionCall ? "tool-calls" : "unknown" + 529 : } + 530 : + 531 23 : const openaiMetadata = (metadata: Record<string, unknown>): ProviderMetadata => ({ openai: metadata }) + 532 : + 533 : // Hosted tool items (provider-executed) ship their typed input + status + + 534 : // result fields all in one item. We expose them as a `tool-call` + + 535 : // `tool-result` pair so consumers can treat them uniformly with client tools, + 536 : // only differentiated by `providerExecuted: true`. + 537 : // + 538 : // One record per OpenAI Responses item type that represents a hosted + 539 : // (provider-executed) tool call: the common name we surface, plus an `input` + 540 : // extractor that picks the fields the model actually populated for that tool. + 541 : // Falling back to `{}` when an entry isn't fully typed keeps unknown tools + 542 : // observable without rolling a per-tool schema. + 543 23 : const HOSTED_TOOLS = { + 544 50 : web_search_call: { name: "web_search", input: (item) => item.action ?? {} }, + 545 66 : web_search_preview_call: { name: "web_search_preview", input: (item) => item.action ?? {} }, + 546 52 : file_search_call: { name: "file_search", input: (item) => ({ queries: item.queries ?? [] }) }, + 547 28 : code_interpreter_call: { + 548 29 : name: "code_interpreter", + 549 8 : input: (item) => ({ code: item.code, container_id: item.container_id }), + 550 4 : }, + 551 54 : computer_use_call: { name: "computer_use", input: (item) => item.action ?? {} }, + 552 62 : image_generation_call: { name: "image_generation", input: () => ({}) }, + 553 15 : mcp_call: { + 554 16 : name: "mcp", + 555 8 : input: (item) => ({ server_label: item.server_label, name: item.name, arguments: item.arguments }), + 556 4 : }, + 557 50 : local_shell_call: { name: "local_shell", input: (item) => item.action ?? {} }, + 558 2 : } as const satisfies Record< + 559 : string, + 560 : { readonly name: string; readonly input: (item: OpenAIResponsesStreamItem) => unknown } + 561 : > + 562 : + 563 : type HostedToolType = keyof typeof HOSTED_TOOLS + 564 : + 565 0 : const isHostedToolItem = ( + 566 0 : item: OpenAIResponsesStreamItem, + 567 0 : ): item is OpenAIResponsesStreamItem & { type: HostedToolType; id: string } => + 568 2 : item.type in HOSTED_TOOLS && typeof item.id === "string" && item.id.length > 0 + 569 : + 570 0 : const isReasoningItem = ( + 571 0 : item: OpenAIResponsesStreamItem, + 572 0 : ): item is OpenAIResponsesStreamItem & { type: "reasoning"; id: string } => + 573 2 : item.type === "reasoning" && typeof item.id === "string" && item.id.length > 0 + 574 : + 575 : // Round-trip the full item as the structured result so consumers can extract + 576 : // outputs / sources / status without re-decoding. + 577 0 : const hostedToolResult = (item: OpenAIResponsesStreamItem) => { + 578 0 : const isError = typeof item.error !== "undefined" && item.error !== null + 579 2 : return isError ? { type: "error" as const, value: item.error } : { type: "json" as const, value: item } + 580 : } + 581 : + 582 0 : const hostedToolEvents = ( + 583 0 : item: OpenAIResponsesStreamItem & { type: HostedToolType; id: string }, + 584 0 : ): ReadonlyArray<LLMEvent> => { + 585 0 : const tool = HOSTED_TOOLS[item.type] + 586 0 : const providerMetadata = openaiMetadata({ itemId: item.id }) + 587 0 : return [ + 588 0 : LLMEvent.toolCall({ + 589 0 : id: item.id, + 590 0 : name: tool.name, + 591 0 : input: tool.input(item), + 592 0 : providerExecuted: true, + 593 0 : providerMetadata, + 594 0 : }), + 595 0 : LLMEvent.toolResult({ + 596 0 : id: item.id, + 597 0 : name: tool.name, + 598 0 : result: hostedToolResult(item), + 599 0 : providerExecuted: true, + 600 0 : providerMetadata, + 601 0 : }), + 602 2 : ] + 603 : } + 604 : + 605 : type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>] + 606 : + 607 21 : const NO_EVENTS: StepResult["1"] = [] + 608 : + 609 : // `response.completed` / `response.incomplete` are clean finishes that emit a + 610 : // `finish` event; `response.failed` is a hard failure that emits a + 611 : // `provider-error`. All three end the stream — kept in one set so `step` and + 612 : // the protocol's `terminal` predicate stay in sync. + 613 97 : const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"]) + 614 : + 615 0 : const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { + 616 0 : if (!event.delta) return [state, NO_EVENTS] + 617 0 : const events: LLMEvent[] = [] + 618 0 : return [ + 619 0 : { ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, event.item_id ?? "text-0", event.delta) }, + 620 0 : events, + 621 2 : ] + 622 : } + 623 : + 624 0 : const onReasoningDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { + 625 0 : if (!event.delta) return [state, NO_EVENTS] + 626 0 : const events: LLMEvent[] = [] + 627 0 : const itemID = event.item_id ?? "reasoning-0" + 628 0 : const id = + 629 0 : event.summary_index !== undefined || state.reasoningItems[itemID] ? `${itemID}:${event.summary_index ?? 0}` : itemID + 630 0 : return [ + 631 0 : { + 632 0 : ...state, + 633 0 : lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, id, event.delta), + 634 0 : }, + 635 0 : events, + 636 2 : ] + 637 : } + 638 : + 639 24 : const onReasoningDone = (state: ParserState, _event: OpenAIResponsesEvent): StepResult => [state, NO_EVENTS] + 640 : + 641 0 : const reasoningMetadata = (item: OpenAIResponsesStreamItem & { id: string }) => + 642 2 : openaiMetadata({ itemId: item.id, reasoningEncryptedContent: item.encrypted_content ?? null }) + 643 : + 644 : // OpenAI Responses streams reasoning items in a stable order: + 645 : // `output_item.added` (reasoning) → + 646 : // `reasoning_summary_part.added` (index=0) → + 647 : // `reasoning_summary_text.delta` → + 648 : // `reasoning_summary_part.done` (index=0) → + 649 : // (repeat for index>0) → + 650 : // `output_item.done` (reasoning). + 651 : // The handlers below rely on this ordering: `onOutputItemAdded` seeds the + 652 : // per-item entry, `onReasoningSummaryPartAdded` for `summary_index === 0` + 653 : // short-circuits when the entry already exists, and higher-index handlers + 654 : // fold against the same entry. Behaviour for out-of-order events is + 655 : // best-effort, not guaranteed. + 656 0 : const onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { + 657 0 : const item = event.item + 658 0 : if (item && isReasoningItem(item)) { + 659 0 : const events: LLMEvent[] = [] + 660 0 : return [ + 661 0 : { + 662 0 : ...state, + 663 0 : lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${item.id}:0`, reasoningMetadata(item)), + 664 0 : reasoningItems: { + 665 0 : ...state.reasoningItems, + 666 0 : [item.id]: { encryptedContent: item.encrypted_content, summaryParts: { 0: "active" } }, + 667 0 : }, + 668 0 : }, + 669 0 : events, + 670 0 : ] + 671 0 : } + 672 0 : if (item?.type !== "function_call" || !item.id) return [state, NO_EVENTS] + 673 0 : const providerMetadata = openaiMetadata({ itemId: item.id }) + 674 0 : const events: LLMEvent[] = [] + 675 0 : const lifecycle = Lifecycle.stepStart(state.lifecycle, events) + 676 0 : return [ + 677 0 : { + 678 0 : ...state, + 679 0 : lifecycle, + 680 0 : hasFunctionCall: state.hasFunctionCall, + 681 0 : tools: ToolStream.start(state.tools, item.id, { + 682 0 : id: item.call_id ?? item.id, + 683 0 : name: item.name ?? "", + 684 0 : input: item.arguments ?? "", + 685 0 : providerMetadata, + 686 0 : }), + 687 0 : }, + 688 0 : [...events, LLMEvent.toolInputStart({ id: item.call_id ?? item.id, name: item.name ?? "", providerMetadata })], + 689 2 : ] + 690 : } + 691 : + 692 0 : const onReasoningSummaryPartAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { + 693 0 : if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS] + 694 0 : const item = state.reasoningItems[event.item_id] ?? { encryptedContent: undefined, summaryParts: {} } + 695 0 : if (event.summary_index === 0) { + 696 0 : if (state.reasoningItems[event.item_id]) return [state, NO_EVENTS] + 697 0 : const events: LLMEvent[] = [] + 698 0 : return [ + 699 0 : { + 700 0 : ...state, + 701 0 : lifecycle: Lifecycle.reasoningStart( + 702 0 : state.lifecycle, + 703 0 : events, + 704 0 : `${event.item_id}:0`, + 705 0 : openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: null }), + 706 0 : ), + 707 0 : reasoningItems: { + 708 0 : ...state.reasoningItems, + 709 0 : [event.item_id]: { ...item, summaryParts: { 0: "active" } }, + 710 0 : }, + 711 0 : }, + 712 0 : events, + 713 0 : ] + 714 0 : } + 715 0 : + 716 0 : const events: LLMEvent[] = [] + 717 0 : const closed = Object.entries(item.summaryParts) + 718 0 : .filter((entry) => entry[1] === "can-conclude") + 719 0 : .reduce( + 720 0 : (lifecycle, entry) => + 721 0 : Lifecycle.reasoningEnd( + 722 0 : lifecycle, + 723 0 : events, + 724 0 : `${event.item_id}:${entry[0]}`, + 725 0 : openaiMetadata({ itemId: event.item_id }), + 726 0 : ), + 727 0 : state.lifecycle, + 728 0 : ) + 729 0 : return [ + 730 0 : { + 731 0 : ...state, + 732 0 : lifecycle: Lifecycle.reasoningStart( + 733 0 : closed, + 734 0 : events, + 735 0 : `${event.item_id}:${event.summary_index}`, + 736 0 : openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null }), + 737 0 : ), + 738 0 : reasoningItems: { + 739 0 : ...state.reasoningItems, + 740 0 : [event.item_id]: { + 741 0 : ...item, + 742 0 : summaryParts: { + 743 0 : ...Object.fromEntries( + 744 0 : Object.entries(item.summaryParts).map((entry) => + 745 0 : entry[1] === "can-conclude" ? [entry[0], "concluded" as const] : entry, + 746 0 : ), + 747 0 : ), + 748 0 : [event.summary_index]: "active", + 749 0 : }, + 750 0 : }, + 751 0 : }, + 752 0 : }, + 753 0 : events, + 754 2 : ] + 755 : } + 756 : + 757 0 : const onReasoningSummaryPartDone = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { + 758 0 : if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS] + 759 0 : const item = state.reasoningItems[event.item_id] + 760 0 : if (!item) return [state, NO_EVENTS] + 761 0 : const events: LLMEvent[] = [] + 762 0 : return [ + 763 0 : { + 764 0 : ...state, + 765 0 : lifecycle: + 766 0 : state.store !== false + 767 0 : ? Lifecycle.reasoningEnd( + 768 0 : state.lifecycle, + 769 0 : events, + 770 0 : `${event.item_id}:${event.summary_index}`, + 771 0 : openaiMetadata({ itemId: event.item_id }), + 772 0 : ) + 773 0 : : state.lifecycle, + 774 0 : reasoningItems: { + 775 0 : ...state.reasoningItems, + 776 0 : [event.item_id]: { + 777 0 : ...item, + 778 0 : summaryParts: { + 779 0 : ...item.summaryParts, + 780 0 : [event.summary_index]: state.store !== false ? "concluded" : "can-conclude", + 781 0 : }, + 782 0 : }, + 783 0 : }, + 784 0 : }, + 785 0 : events, + 786 2 : ] + 787 : } + 788 : + 789 0 : const onFunctionCallArgumentsDelta = Effect.fn("OpenAIResponses.onFunctionCallArgumentsDelta")(function* ( + 790 0 : state: ParserState, + 791 0 : event: OpenAIResponsesEvent, + 792 0 : ) { + 793 0 : if (!event.item_id || !event.delta) return [state, NO_EVENTS] satisfies StepResult + 794 0 : const result = ToolStream.appendExisting( + 795 0 : ADAPTER, + 796 0 : state.tools, + 797 0 : event.item_id, + 798 0 : event.delta, + 799 0 : "OpenAI Responses tool argument delta is missing its tool call", + 800 0 : ) + 801 0 : if (ToolStream.isError(result)) return yield* result + 802 0 : const events: LLMEvent[] = [] + 803 0 : const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle + 804 0 : events.push(...result.events) + 805 : return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult + 806 3 : }) + 807 : + 808 0 : const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function* ( + 809 0 : state: ParserState, + 810 0 : event: OpenAIResponsesEvent, + 811 0 : ) { + 812 0 : const item = event.item + 813 0 : if (!item) return [state, NO_EVENTS] satisfies StepResult + 814 0 : + 815 0 : if (item.type === "function_call") { + 816 0 : if (!item.id || !item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult + 817 0 : const tools = state.tools[item.id] + 818 0 : ? state.tools + 819 0 : : ToolStream.start(state.tools, item.id, { id: item.call_id, name: item.name }) + 820 0 : const result = + 821 0 : item.arguments === undefined + 822 0 : ? yield* ToolStream.finish(ADAPTER, tools, item.id) + 823 0 : : yield* ToolStream.finishWithInput(ADAPTER, tools, item.id, item.arguments) + 824 0 : const events: LLMEvent[] = [] + 825 0 : const resultEvents = result.events ?? [] + 826 0 : const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle + 827 0 : events.push(...resultEvents) + 828 0 : return [ + 829 0 : { + 830 0 : ...state, + 831 0 : lifecycle, + 832 0 : hasFunctionCall: resultEvents.some(LLMEvent.is.toolCall) ? true : state.hasFunctionCall, + 833 0 : tools: result.tools, + 834 0 : }, + 835 0 : events, + 836 0 : ] satisfies StepResult + 837 0 : } + 838 0 : + 839 0 : if (isHostedToolItem(item)) { + 840 0 : const events: LLMEvent[] = [] + 841 0 : const lifecycle = Lifecycle.stepStart(state.lifecycle, events) + 842 0 : events.push(...hostedToolEvents(item)) + 843 0 : return [{ ...state, lifecycle }, events] satisfies StepResult + 844 0 : } + 845 0 : + 846 0 : if (isReasoningItem(item)) { + 847 0 : const events: LLMEvent[] = [] + 848 0 : const providerMetadata = reasoningMetadata(item) + 849 0 : const reasoningItem = state.reasoningItems[item.id] + 850 0 : if (reasoningItem) { + 851 0 : const lifecycle = Object.entries(reasoningItem.summaryParts) + 852 0 : .filter((entry) => entry[1] === "active" || entry[1] === "can-conclude") + 853 0 : .reduce( + 854 0 : (lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, providerMetadata), + 855 0 : state.lifecycle, + 856 0 : ) + 857 0 : const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems + 858 0 : return [{ ...state, lifecycle, reasoningItems }, events] satisfies StepResult + 859 0 : } + 860 0 : if (!state.lifecycle.reasoning.has(item.id)) { + 861 0 : const lifecycle = Lifecycle.stepStart(state.lifecycle, events) + 862 0 : events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata })) + 863 0 : events.push(LLMEvent.reasoningEnd({ id: item.id, providerMetadata })) + 864 0 : return [{ ...state, lifecycle }, events] satisfies StepResult + 865 0 : } + 866 0 : return [ + 867 0 : { ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, item.id, providerMetadata) }, + 868 0 : events, + 869 0 : ] satisfies StepResult + 870 0 : } + 871 0 : + 872 : return [state, NO_EVENTS] satisfies StepResult + 873 3 : }) + 874 : + 875 0 : const onResponseFinish = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { + 876 0 : const events: LLMEvent[] = [] + 877 0 : const lifecycle = Lifecycle.finish(state.lifecycle, events, { + 878 0 : reason: mapFinishReason(event, state.hasFunctionCall), + 879 0 : usage: mapUsage(event.response?.usage), + 880 0 : providerMetadata: + 881 0 : event.response?.id || event.response?.service_tier + 882 0 : ? openaiMetadata({ + 883 0 : responseId: event.response.id, + 884 0 : serviceTier: event.response.service_tier, + 885 0 : }) + 886 0 : : undefined, + 887 0 : }) + 888 2 : return [{ ...state, lifecycle }, events] + 889 : } + 890 : + 891 : // Build a single human-readable message from whatever the provider supplied. + 892 : // When both code and message are present, prefix the code so consumers see + 893 : // the failure mode (e.g. `rate_limit_exceeded: Slow down`) instead of just + 894 : // the bare message — production rate limits and context-length failures used + 895 : // to be indistinguishable from generic stream drops. + 896 0 : const providerErrorMessage = (event: OpenAIResponsesEvent, fallback: string): string => { + 897 0 : const nested = event.response?.error ?? undefined + 898 0 : const message = event.message || nested?.message || undefined + 899 0 : const code = event.code || nested?.code || undefined + 900 0 : if (message && code) return `${code}: ${message}` + 901 2 : return message || code || fallback + 902 : } + 903 : + 904 0 : const providerError = (event: OpenAIResponsesEvent, fallback: string) => { + 905 0 : const code = event.code || event.response?.error?.code || undefined + 906 0 : const message = providerErrorMessage(event, fallback) + 907 0 : return LLMEvent.providerError({ + 908 0 : message, + 909 0 : classification: code === "context_length_exceeded" || isContextOverflow(message) ? "context-overflow" : undefined, + 910 2 : }) + 911 : } + 912 : + 913 0 : const onResponseFailed = (state: ParserState, event: OpenAIResponsesEvent): StepResult => [ + 914 0 : state, + 915 : [providerError(event, "OpenAI Responses response failed")], + 916 2 : ] + 917 : + 918 0 : const onError = (state: ParserState, event: OpenAIResponsesEvent): StepResult => [ + 919 0 : state, + 920 : [providerError(event, "OpenAI Responses stream error")], + 921 2 : ] + 922 : + 923 0 : const step = (state: ParserState, event: OpenAIResponsesEvent) => { + 924 0 : if (event.type === "response.output_text.delta") return Effect.succeed(onOutputTextDelta(state, event)) + 925 0 : if ( + 926 0 : event.type === "response.reasoning_text.delta" || + 927 0 : event.type === "response.reasoning_summary.delta" || + 928 0 : event.type === "response.reasoning_summary_text.delta" + 929 0 : ) + 930 0 : return Effect.succeed(onReasoningDelta(state, event)) + 931 0 : if ( + 932 0 : event.type === "response.reasoning_text.done" || + 933 0 : event.type === "response.reasoning_summary.done" || + 934 0 : event.type === "response.reasoning_summary_text.done" + 935 0 : ) + 936 0 : return Effect.succeed(onReasoningDone(state, event)) + 937 0 : if (event.type === "response.reasoning_summary_part.added") + 938 0 : return Effect.succeed(onReasoningSummaryPartAdded(state, event)) + 939 0 : if (event.type === "response.reasoning_summary_part.done") + 940 0 : return Effect.succeed(onReasoningSummaryPartDone(state, event)) + 941 0 : if (event.type === "response.output_item.added") return Effect.succeed(onOutputItemAdded(state, event)) + 942 0 : if (event.type === "response.function_call_arguments.delta") return onFunctionCallArgumentsDelta(state, event) + 943 0 : if (event.type === "response.output_item.done") return onOutputItemDone(state, event) + 944 0 : if (event.type === "response.completed" || event.type === "response.incomplete") + 945 0 : return Effect.succeed(onResponseFinish(state, event)) + 946 0 : if (event.type === "response.failed") return Effect.succeed(onResponseFailed(state, event)) + 947 0 : if (event.type === "error") return Effect.succeed(onError(state, event)) + 948 2 : return Effect.succeed<StepResult>([state, NO_EVENTS]) + 949 : } + 950 : + 951 : // ============================================================================= + 952 : // Protocol And OpenAI Route + 953 : // ============================================================================= + 954 : /** + 955 : * The OpenAI Responses protocol — request body construction, body schema, and + 956 : * the streaming-event state machine. Used by native OpenAI and (once + 957 : * registered) Azure OpenAI Responses. + 958 : */ + 959 40 : export const protocol = Protocol.make({ + 960 14 : id: ADAPTER, + 961 11 : body: { + 962 32 : schema: OpenAIResponsesBody, + 963 19 : from: fromRequest, + 964 4 : }, + 965 13 : stream: { + 966 52 : event: Protocol.jsonEvent(OpenAIResponsesEvent), + 967 0 : initial: (request) => ({ + 968 0 : hasFunctionCall: false, + 969 0 : tools: ToolStream.empty<string>(), + 970 0 : lifecycle: Lifecycle.initial(), + 971 0 : reasoningItems: {}, + 972 0 : store: OpenAIOptions.store(request), + 973 5 : }), + 974 9 : step, + 975 11 : terminal: (event) => TERMINAL_TYPES.has(event.type), + 976 2 : }, + 977 3 : }) + 978 : + 979 68 : const endpoint = Endpoint.path<OpenAIResponsesBody>(PATH, { baseURL: DEFAULT_BASE_URL }) + 980 23 : const auth = Auth.none + 981 : + 982 58 : export const httpTransport = HttpTransport.sseJson.with<OpenAIResponsesBody>() + 983 : + 984 34 : export const route = Route.make({ + 985 14 : id: ADAPTER, + 986 21 : provider: "openai", + 987 11 : protocol, + 988 11 : endpoint, + 989 7 : auth, + 990 27 : transport: httpTransport, + 991 60 : defaults: { providerOptions: { openai: { store: false } } }, + 992 3 : }) + 993 : + 994 120 : const decodeWebSocketMessage = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIResponsesWebSocketMessage)) + 995 : + 996 0 : const webSocketMessage = (body: OpenAIResponsesBody | Record<string, unknown>) => + 997 0 : Effect.gen(function* () { + 998 0 : if (!ProviderShared.isRecord(body)) + 999 0 : return yield* ProviderShared.invalidRequest("OpenAI Responses WebSocket body must be a JSON object") + 1000 0 : const { stream: _stream, ...message } = body + 1001 : return yield* decodeWebSocketMessage({ ...message, type: "response.create" }) + 1002 2 : }) + 1003 : + 1004 71 : export const webSocketTransport = WebSocketTransport.jsonTransport.with< + 1005 : OpenAIResponsesBody, + 1006 : OpenAIResponsesWebSocketMessage + 1007 3 : >({ + 1008 30 : toMessage: webSocketMessage, + 1009 38 : encodeMessage: encodeWebSocketMessage, + 1010 3 : }) + 1011 : + 1012 43 : export const webSocketRoute = Route.make({ + 1013 29 : id: `${ADAPTER}-websocket`, + 1014 21 : provider: "openai", + 1015 11 : protocol, + 1016 11 : endpoint, + 1017 7 : auth, + 1018 32 : transport: webSocketTransport, + 1019 60 : defaults: { providerOptions: { openai: { store: false } } }, + 1020 3 : }) + 1021 : + 1022 53 : export * as OpenAIResponses from "./openai-responses" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 37 : import { Buffer } from "node:buffer" + 2 48 : import { Effect, Schema, Stream } from "effect" + 3 52 : import * as Sse from "effect/unstable/encoding/Sse" + 4 66 : import { Headers, HttpClientRequest } from "effect/unstable/http" + 5 90 : import { + 6 : InvalidProviderOutputReason, + 7 : InvalidRequestReason, + 8 : LLMError, + 9 : type ContentPart, + 10 : type LLMRequest, + 11 : type MediaPart, + 12 : type ToolFileContent, + 13 : type TextPart, + 14 : type ToolResultPart, + 15 : } from "../schema" + 16 43 : import { isRecord } from "../utils/record" + 17 20 : export { isRecord } + 18 : + 19 58 : export const Json = Schema.fromJsonString(Schema.Unknown) + 20 57 : export const decodeJson = Schema.decodeUnknownSync(Json) + 21 50 : export const encodeJson = Schema.encodeSync(Json) + 22 38 : const isJson = Schema.is(Schema.Json) + 23 71 : export const JsonObject = Schema.Record(Schema.String, Schema.Unknown) + 24 77 : export const optionalArray = <const S extends Schema.Top>(schema: S) => Schema.optional(Schema.Array(schema)) + 25 77 : export const optionalNull = <const S extends Schema.Top>(schema: S) => Schema.optional(Schema.NullOr(schema)) + 26 : + 27 : /** + 28 : * Streaming tool-call accumulator. Adapters that build a tool call across + 29 : * multiple `tool-input-delta` chunks store the partial JSON input string here + 30 : * and finalize it with `parseToolInput` once the call completes. + 31 : */ + 32 : export interface ToolAccumulator { + 33 : readonly id: string + 34 : readonly name: string + 35 : readonly input: string + 36 : } + 37 : + 38 : /** + 39 : * `Usage.totalTokens` policy shared by every route. Honors a provider- + 40 : * supplied total; otherwise falls back to `inputTokens + outputTokens` only + 41 : * when at least one is defined. Returns `undefined` when neither input nor + 42 : * output is known so routes don't publish a misleading `0`. + 43 : * + 44 : * Under the additive `LLM.Usage` contract, `inputTokens` and `outputTokens` + 45 : * are the non-cached input and visible output only. The provider-supplied + 46 : * `total` is the source of truth when present; the computed fallback + 47 : * under-counts cache and reasoning by design and exists mainly so + 48 : * Anthropic-style providers (which don't surface a total) still get a + 49 : * sensible aggregate on the input + output axes. + 50 : */ + 51 26 : export const totalTokens = ( + 52 13 : inputTokens: number | undefined, + 53 14 : outputTokens: number | undefined, + 54 10 : total: number | undefined, + 55 3 : ) => { + 56 39 : if (total !== undefined) return total + 57 0 : if (inputTokens === undefined && outputTokens === undefined) return undefined + 58 2 : return (inputTokens ?? 0) + (outputTokens ?? 0) + 59 : } + 60 : + 61 : /** + 62 : * Subtract `subtrahend` from `total`, clamping to zero if the provider + 63 : * reports a non-sensical breakdown (e.g. `cached_tokens > prompt_tokens`). + 64 : * Used by protocol mappers when deriving a non-overlapping breakdown field + 65 : * from a provider's inclusive total — `nonCachedInputTokens` from + 66 : * `inputTokens - cacheReadInputTokens - cacheWriteInputTokens`. + 67 : * + 68 : * If `total` is `undefined`, returns `undefined` (we don't fabricate + 69 : * counts). If `subtrahend` is `undefined`, returns `total` unchanged. The + 70 : * provider-native breakdown stays available on `Usage.native` for debugging. + 71 : */ + 72 54 : export const subtractTokens = (total: number | undefined, subtrahend: number | undefined): number | undefined => { + 73 29 : if (total === undefined) return undefined + 74 34 : if (subtrahend === undefined) return total + 75 40 : return Math.max(0, total - subtrahend) + 76 : } + 77 : + 78 : /** + 79 : * Sum a list of optional token counts, returning `undefined` only when + 80 : * every value is `undefined` (so we don't fabricate a `0`). Used by + 81 : * protocol mappers to derive the inclusive `inputTokens` total from a + 82 : * provider that natively reports a non-overlapping breakdown + 83 : * (e.g. Anthropic, whose `input_tokens` is already non-cached only). + 84 : */ + 85 0 : export const sumTokens = (...values: ReadonlyArray<number | undefined>): number | undefined => { + 86 0 : if (values.every((value) => value === undefined)) return undefined + 87 2 : return values.reduce((acc: number, value) => acc + (value ?? 0), 0) + 88 : } + 89 : + 90 0 : export const eventError = (route: string, message: string, raw?: string) => + 91 0 : new LLMError({ + 92 0 : module: "ProviderShared", + 93 0 : method: "stream", + 94 : reason: new InvalidProviderOutputReason({ route, message, raw }), + 95 2 : }) + 96 : + 97 0 : export const parseJson = (route: string, input: string, message: string) => + 98 0 : Effect.try({ + 99 0 : try: () => decodeJson(input), + 100 : catch: () => eventError(route, message, input), + 101 2 : }) + 102 : + 103 : /** + 104 : * Join the `text` field of a list of parts with newlines. Used by routes + 105 : * that flatten system / message content arrays into a single provider string + 106 : * (OpenAI Chat `system` content, OpenAI Responses `system` content, Gemini + 107 : * `systemInstruction.parts[].text`). + 108 : */ + 109 24 : export const joinText = (parts: ReadonlyArray<{ readonly text: string }>) => parts.map((part) => part.text).join("\n") + 110 : + 111 0 : const escapeSystemUpdateText = (text: string) => + 112 2 : text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">") + 113 : + 114 : /** + 115 : * Stable fallback representation for chronological `Message.system(...)` + 116 : * updates on routes that do not support that privileged role natively. The + 117 : * wrapper remains visibly lower-authority user text, preserves the original + 118 : * temporal position, and XML-escapes content so it cannot close the wrapper. + 119 : */ + 120 0 : export const wrapSystemUpdate = (parts: ReadonlyArray<{ readonly text: string }>) => + 121 2 : `<system-update>\n${escapeSystemUpdateText(joinText(parts))}\n</system-update>` + 122 : + 123 : /** + 124 : * Chronological system updates deliberately accept text only. Do not insert + 125 : * raw retrieved, tool, or web content into privileged updates: keep untrusted + 126 : * data in ordinary user/tool messages instead. + 127 : */ + 128 0 : export const systemUpdateText = Effect.fn("ProviderShared.systemUpdateText")(function* ( + 129 0 : route: string, + 130 0 : message: LLMRequest["messages"][number], + 131 0 : ) { + 132 0 : const content: TextPart[] = [] + 133 0 : for (const part of message.content) { + 134 0 : if (!supportsContent(part, ["text"])) return yield* unsupportedContent(route, "system", ["text"]) + 135 0 : content.push(part) + 136 0 : } + 137 : return content + 138 3 : }) + 139 : + 140 : /** Lower an unsupported privileged update into visible, in-order user text. */ + 141 0 : export const wrappedSystemUpdate = Effect.fn("ProviderShared.wrappedSystemUpdate")(function* ( + 142 0 : route: string, + 143 0 : message: LLMRequest["messages"][number], + 144 0 : ) { + 145 0 : const content = yield* systemUpdateText(route, message) + 146 : return { type: "text" as const, text: wrapSystemUpdate(content), cache: content.at(-1)?.cache } + 147 3 : }) + 148 : + 149 : /** + 150 : * Parse the streamed JSON input of a tool call. Treats an empty string as + 151 : * `"{}"` — providers occasionally finish a tool call without ever emitting + 152 : * input deltas (e.g. zero-arg tools). The error message is uniform across + 153 : * routes: `Invalid JSON input for <route> tool call <name>`. + 154 : */ + 155 0 : export const parseToolInput = (route: string, name: string, raw: string) => + 156 2 : parseJson(route, raw || "{}", `Invalid JSON input for ${route} tool call ${name}`) + 157 : + 158 82 : export const IMAGE_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp"] as const + 159 74 : export const VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"] as const + 160 108 : export const AUDIO_MIMES = ["audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac"] as const + 161 76 : export const MEDIA_MIMES = [...IMAGE_MIMES, ...VIDEO_MIMES, ...AUDIO_MIMES] as const + 162 48 : export const MAX_MEDIA_ENCODED_BYTES = 28 * 1024 * 1024 + 163 48 : export const MAX_MEDIA_DECODED_BYTES = 20 * 1024 * 1024 + 164 : + 165 89 : const base64Pattern = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/ + 166 : + 167 : export interface ValidatedMedia { + 168 : readonly mime: string + 169 : readonly base64: string + 170 : readonly dataUrl: string + 171 : readonly bytes: Uint8Array + 172 : } + 173 : + 174 0 : export const validateMedia = Effect.fn("ProviderShared.validateMedia")(function* ( + 175 0 : route: string, + 176 0 : part: MediaPart, + 177 0 : supportedMimes: ReadonlySet<string>, + 178 0 : ) { + 179 0 : const mime = part.mediaType.toLowerCase() + 180 0 : if (!supportedMimes.has(mime)) return yield* invalidRequest(`${route} does not support media type ${part.mediaType}`) + 181 0 : + 182 0 : let base64: string + 183 0 : if (typeof part.data !== "string") { + 184 0 : if (part.data.byteLength > MAX_MEDIA_DECODED_BYTES) + 185 0 : return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_DECODED_BYTES} byte decoded limit`) + 186 0 : base64 = Buffer.from(part.data).toString("base64") + 187 0 : } else if (part.data.startsWith("data:")) { + 188 0 : const match = /^data:([^;,]+);base64,([A-Za-z0-9+/]*={0,2})$/s.exec(part.data) + 189 0 : if (!match) return yield* invalidRequest(`${route} media data URL must contain valid base64`) + 190 0 : if (match[1]!.toLowerCase() !== mime) + 191 0 : return yield* invalidRequest(`${route} media type ${part.mediaType} does not match data URL type ${match[1]}`) + 192 0 : base64 = match[2]! + 193 0 : } else { + 194 0 : base64 = part.data + 195 0 : } + 196 0 : + 197 0 : if (Buffer.byteLength(base64, "utf8") > MAX_MEDIA_ENCODED_BYTES) + 198 0 : return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_ENCODED_BYTES} byte encoded limit`) + 199 0 : if (!base64 || base64.length % 4 !== 0 || !base64Pattern.test(base64)) + 200 0 : return yield* invalidRequest(`${route} media must contain valid base64`) + 201 0 : const bytes = Buffer.from(base64, "base64") + 202 0 : if (bytes.byteLength > MAX_MEDIA_DECODED_BYTES) + 203 0 : return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_DECODED_BYTES} byte decoded limit`) + 204 0 : if (bytes.toString("base64") !== base64) return yield* invalidRequest(`${route} media must contain canonical base64`) + 205 : return { mime, base64, dataUrl: `data:${mime};base64,${base64}`, bytes } satisfies ValidatedMedia + 206 3 : }) + 207 : + 208 0 : export const validateToolFile = (route: string, part: ToolFileContent, supportedMimes: ReadonlySet<string>) => + 209 2 : validateMedia(route, { type: "media", mediaType: part.mime, data: part.uri, filename: part.name }, supportedMimes) + 210 : + 211 62 : export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "") + 212 : + 213 0 : export const toolResultText = (part: ToolResultPart) => { + 214 0 : if (part.result.type === "text") return String(part.result.value) + 215 0 : if (part.result.type === "error") { + 216 0 : const value = part.result.value + 217 0 : const prototype = + 218 0 : typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) + 219 0 : const structured = Array.isArray(value) || prototype === Object.prototype || prototype === null + 220 0 : return structured && isJson(value) ? encodeJson(value) : String(value) + 221 0 : } + 222 2 : return encodeJson(part.result.value) + 223 : } + 224 : + 225 0 : export const errorText = (error: unknown) => { + 226 0 : if (error instanceof Error) return error.message + 227 0 : if (typeof error === "string") return error + 228 0 : if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") return String(error) + 229 0 : if (error === null) return "null" + 230 0 : if (error === undefined) return "undefined" + 231 2 : return "Unknown stream error" + 232 : } + 233 : + 234 : /** + 235 : * `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel + 236 : * decoder, and drops empty / `[DONE]` keep-alive events so the downstream + 237 : * `decodeChunk` sees one JSON string per element. The SSE channel emits a + 238 : * `Retry` control event on its error channel; we drop it here (we don't + 239 : * implement client-driven retries) so the public error channel stays + 240 : * `LLMError`. + 241 : */ + 242 35 : export const sseFraming = (bytes: Stream.Stream<Uint8Array, LLMError>): Stream.Stream<string, LLMError> => + 243 11 : bytes.pipe( + 244 21 : Stream.decodeText(), + 245 41 : Stream.pipeThroughChannel(Sse.decode()), + 246 27 : Stream.catchTag("Retry", () => Stream.empty), + 247 74 : Stream.filter((event) => event.data.length > 0 && event.data !== "[DONE]"), + 248 31 : Stream.map((event) => event.data), + 249 2 : ) + 250 : + 251 : /** + 252 : * Canonical invalid-request constructor. Lift one-line `const invalid = + 253 : * (message) => invalidRequest(message)` aliases out of every + 254 : * route so the error constructor lives in one place. If we ever extend + 255 : * `InvalidRequestReason` with route context or trace metadata, the change + 256 : * lands here. + 257 : */ + 258 0 : export const invalidRequest = (message: string) => + 259 0 : new LLMError({ + 260 0 : module: "ProviderShared", + 261 0 : method: "request", + 262 : reason: new InvalidRequestReason({ message }), + 263 2 : }) + 264 : + 265 0 : export const matchToolChoice = <Auto, None, Required, Tool>( + 266 0 : route: string, + 267 0 : toolChoice: NonNullable<LLMRequest["toolChoice"]>, + 268 0 : cases: { + 269 0 : readonly auto: () => Auto + 270 0 : readonly none: () => None + 271 0 : readonly required: () => Required + 272 0 : readonly tool: (name: string) => Tool + 273 0 : }, + 274 0 : ) => + 275 0 : Effect.gen(function* () { + 276 0 : if (toolChoice.type === "auto") return cases.auto() + 277 0 : if (toolChoice.type === "none") return cases.none() + 278 0 : if (toolChoice.type === "required") return cases.required() + 279 0 : if (!toolChoice.name) return yield* invalidRequest(`${route} tool choice requires a tool name`) + 280 : return cases.tool(toolChoice.name) + 281 2 : }) + 282 : + 283 : type ContentType = ContentPart["type"] + 284 : + 285 0 : const formatContentTypes = (types: ReadonlyArray<ContentType>) => { + 286 0 : if (types.length <= 1) return types[0] ?? "" + 287 0 : if (types.length === 2) return `${types[0]} and ${types[1]}` + 288 2 : return `${types.slice(0, -1).join(", ")}, and ${types.at(-1)}` + 289 : } + 290 : + 291 0 : export const supportsContent = <const Type extends ContentType>( + 292 0 : part: ContentPart, + 293 0 : types: ReadonlyArray<Type>, + 294 2 : ): part is Extract<ContentPart, { readonly type: Type }> => (types as ReadonlyArray<ContentType>).includes(part.type) + 295 : + 296 0 : export const unsupportedContent = ( + 297 0 : route: string, + 298 0 : role: LLMRequest["messages"][number]["role"], + 299 0 : types: ReadonlyArray<ContentType>, + 300 2 : ) => invalidRequest(`${route} ${role} messages only support ${formatContentTypes(types)} content for now`) + 301 : + 302 : /** + 303 : * Build a `validate` step from a Schema decoder. Replaces the per-route + 304 : * lambda body `(payload) => decode(payload).pipe(Effect.mapError((e) => + 305 : * invalid(e.message)))`. Any decode error is translated into + 306 : * `LLMError` carrying the original parse-error message. + 307 : */ + 308 26 : export const validateWith = + 309 11 : <A, I, E extends { readonly message: string }>(decode: (input: I) => Effect.Effect<A, E>) => + 310 13 : (payload: I) => + 311 40 : decode(payload).pipe(Effect.mapError((error) => invalidRequest(error.message))) + 312 : + 313 : /** + 314 : * Build an HTTP POST with a JSON body. Sets `content-type: application/json` + 315 : * automatically after caller-supplied headers so routes cannot accidentally + 316 : * send JSON with a stale content type. The body is passed pre-encoded so + 317 : * routes can choose between + 318 : * `Schema.encodeSync(payload)` and `ProviderShared.encodeJson(payload)`. + 319 : */ + 320 33 : export const jsonPost = (input: { readonly url: string; readonly body: string; readonly headers?: Headers.Input }) => + 321 39 : HttpClientRequest.post(input.url).pipe( + 322 113 : HttpClientRequest.setHeaders(Headers.set(Headers.fromInput(input.headers), "content-type", "application/json")), + 323 58 : HttpClientRequest.bodyText(input.body, "application/json"), + 324 2 : ) + 325 : + 326 42 : export * as ProviderShared from "./shared" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : // Shared helpers for provider cache-marker lowering. Anthropic and Bedrock + 2 : // both enforce a 4-breakpoint cap per request and accept the same `5m`/`1h` + 3 : // TTL buckets, so the counter and TTL mapping live here. + 4 : + 5 : export interface Breakpoints { + 6 : remaining: number + 7 : dropped: number + 8 : } + 9 : + 10 30 : export const newBreakpoints = (cap: number): Breakpoints => ({ remaining: cap, dropped: 0 }) + 11 : + 12 : // Returns `"1h"` for any `ttlSeconds >= 3600`, otherwise `undefined` (the + 13 : // provider default 5m). Anthropic & Bedrock both treat anything shorter than + 14 : // an hour as 5m. + 15 0 : export const ttlBucket = (ttlSeconds: number | undefined): "1h" | undefined => + 16 1 : ttlSeconds !== undefined && ttlSeconds >= 3600 ? "1h" : undefined ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 46 : import { isRecord } from "../../utils/record" + 2 : + 3 : // Gemini accepts a JSON Schema-like dialect for tool parameters, but rejects a + 4 : // handful of common JSON Schema shapes. Keep this projection isolated so the + 5 : // Gemini protocol file still reads like the other protocol modules. + 6 29 : const SCHEMA_INTENT_KEYS = [ + 7 9 : "type", + 8 15 : "properties", + 9 10 : "items", + 10 16 : "prefixItems", + 11 9 : "enum", + 12 10 : "const", + 13 9 : "$ref", + 14 25 : "additionalProperties", + 15 22 : "patternProperties", + 16 13 : "required", + 17 8 : "not", + 18 7 : "if", + 19 9 : "then", + 20 7 : "else", + 21 2 : ] + 22 : + 23 0 : const hasCombiner = (schema: unknown) => + 24 2 : isRecord(schema) && (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf) || Array.isArray(schema.allOf)) + 25 : + 26 0 : const hasSchemaIntent = (schema: unknown) => + 27 2 : isRecord(schema) && (hasCombiner(schema) || SCHEMA_INTENT_KEYS.some((key) => key in schema)) + 28 : + 29 0 : const sanitizeNode = (schema: unknown): unknown => { + 30 0 : if (!isRecord(schema)) return Array.isArray(schema) ? schema.map(sanitizeNode) : schema + 31 0 : + 32 0 : const result: Record<string, unknown> = Object.fromEntries( + 33 0 : Object.entries(schema).map(([key, value]) => [ + 34 0 : key, + 35 0 : key === "enum" && Array.isArray(value) ? value.map(String) : sanitizeNode(value), + 36 0 : ]), + 37 0 : ) + 38 0 : + 39 0 : if (Array.isArray(result.enum) && (result.type === "integer" || result.type === "number")) result.type = "string" + 40 0 : + 41 0 : const properties = result.properties + 42 0 : if (result.type === "object" && isRecord(properties) && Array.isArray(result.required)) { + 43 0 : result.required = result.required.filter((field) => typeof field === "string" && field in properties) + 44 0 : } + 45 0 : + 46 0 : if (result.type === "array" && !hasCombiner(result)) { + 47 0 : result.items = result.items ?? {} + 48 0 : if (isRecord(result.items) && !hasSchemaIntent(result.items)) result.items = { ...result.items, type: "string" } + 49 0 : } + 50 0 : + 51 0 : if (typeof result.type === "string" && result.type !== "object" && !hasCombiner(result)) { + 52 0 : delete result.properties + 53 0 : delete result.required + 54 0 : } + 55 0 : + 56 2 : return result + 57 : } + 58 : + 59 0 : const emptyObjectSchema = (schema: Record<string, unknown>) => + 60 0 : schema.type === "object" && + 61 0 : (!isRecord(schema.properties) || Object.keys(schema.properties).length === 0) && + 62 2 : !schema.additionalProperties + 63 : + 64 0 : const projectNode = (schema: unknown): Record<string, unknown> | undefined => { + 65 0 : if (!isRecord(schema)) return undefined + 66 0 : if (emptyObjectSchema(schema)) return undefined + 67 0 : return Object.fromEntries( + 68 0 : [ + 69 0 : ["description", schema.description], + 70 0 : ["required", schema.required], + 71 0 : ["format", schema.format], + 72 0 : ["type", Array.isArray(schema.type) ? schema.type.filter((type) => type !== "null")[0] : schema.type], + 73 0 : ["nullable", Array.isArray(schema.type) && schema.type.includes("null") ? true : undefined], + 74 0 : ["enum", schema.const !== undefined ? [schema.const] : schema.enum], + 75 0 : [ + 76 0 : "properties", + 77 0 : isRecord(schema.properties) + 78 0 : ? Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, projectNode(value)])) + 79 0 : : undefined, + 80 0 : ], + 81 0 : [ + 82 0 : "items", + 83 0 : Array.isArray(schema.items) + 84 0 : ? schema.items.map(projectNode) + 85 0 : : schema.items === undefined + 86 0 : ? undefined + 87 0 : : projectNode(schema.items), + 88 0 : ], + 89 0 : ["allOf", Array.isArray(schema.allOf) ? schema.allOf.map(projectNode) : undefined], + 90 0 : ["anyOf", Array.isArray(schema.anyOf) ? schema.anyOf.map(projectNode) : undefined], + 91 0 : ["oneOf", Array.isArray(schema.oneOf) ? schema.oneOf.map(projectNode) : undefined], + 92 0 : ["minLength", schema.minLength], + 93 0 : ].filter((entry) => entry[1] !== undefined), + 94 2 : ) + 95 : } + 96 : + 97 23 : export const convert = (schema: unknown) => projectNode(sanitizeNode(schema)) + 98 : + 99 56 : export * as GeminiToolSchema from "./gemini-tool-schema" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| cache.ts | +
+ |
+ 66.7 % | +3 | +2 | +|
| gemini-tool-schema.ts | +
+ |
+ 27.9 % | +86 | +24 | +|
| lifecycle.ts | +
+ |
+ 62.2 % | +74 | +46 | +|
| openai-options.ts | +
+ |
+ 96.6 % | +58 | +56 | +|
| tool-schema.ts | +
+ |
+ 21.4 % | +70 | +15 | +|
| tool-stream.ts | +
+ |
+ 14.4 % | +125 | +18 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| tool-stream.ts | +
+ |
+ 14.4 % | +125 | +18 | +|
| tool-schema.ts | +
+ |
+ 21.4 % | +70 | +15 | +|
| gemini-tool-schema.ts | +
+ |
+ 27.9 % | +86 | +24 | +|
| lifecycle.ts | +
+ |
+ 62.2 % | +74 | +46 | +|
| cache.ts | +
+ |
+ 66.7 % | +3 | +2 | +|
| openai-options.ts | +
+ |
+ 96.6 % | +58 | +56 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| cache.ts | +
+ |
+ 66.7 % | +3 | +2 | +|
| gemini-tool-schema.ts | +
+ |
+ 27.9 % | +86 | +24 | +|
| lifecycle.ts | +
+ |
+ 62.2 % | +74 | +46 | +|
| openai-options.ts | +
+ |
+ 96.6 % | +58 | +56 | +|
| tool-schema.ts | +
+ |
+ 21.4 % | +70 | +15 | +|
| tool-stream.ts | +
+ |
+ 14.4 % | +125 | +18 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 40 : import { LLMEvent, type FinishReason, type ProviderMetadata, type Usage } from "../../schema" + 2 : + 3 : export interface State { + 4 : readonly stepStarted: boolean + 5 : readonly text: ReadonlySet<string> + 6 : readonly reasoning: ReadonlySet<string> + 7 : } + 8 : + 9 87 : export const initial = (): State => ({ stepStarted: false, text: new Set(), reasoning: new Set() }) + 10 : + 11 45 : export const stepStart = (state: State, events: LLMEvent[]): State => { + 12 39 : if (state.stepStarted) return state + 13 48 : events.push(LLMEvent.stepStart({ index: 0 })) + 14 40 : return { ...state, stepStarted: true } + 15 : } + 16 : + 17 55 : export const textDelta = (state: State, events: LLMEvent[], id: string, text: string): State => { + 18 43 : const stepped = stepStart(state, events) + 19 30 : if (stepped.text.has(id)) { + 20 50 : events.push(LLMEvent.textDelta({ id, text })) + 21 14 : return stepped + 22 2 : } + 23 76 : events.push(LLMEvent.textStart({ id }), LLMEvent.textDelta({ id, text })) + 24 61 : return { ...stepped, text: new Set([...stepped.text, id]) } + 25 : } + 26 : + 27 0 : export const reasoningStart = ( + 28 0 : state: State, + 29 0 : events: LLMEvent[], + 30 0 : id: string, + 31 0 : providerMetadata?: ProviderMetadata, + 32 0 : ): State => { + 33 0 : if (state.reasoning.has(id)) return state + 34 0 : const stepped = stepStart(state, events) + 35 0 : events.push(LLMEvent.reasoningStart({ id, providerMetadata })) + 36 2 : return { ...stepped, reasoning: new Set([...stepped.reasoning, id]) } + 37 : } + 38 : + 39 0 : export const reasoningDelta = ( + 40 0 : state: State, + 41 0 : events: LLMEvent[], + 42 0 : id: string, + 43 0 : text: string, + 44 0 : providerMetadata?: ProviderMetadata, + 45 0 : ): State => { + 46 0 : const started = reasoningStart(state, events, id, providerMetadata) + 47 0 : events.push(LLMEvent.reasoningDelta({ id, text })) + 48 2 : return started + 49 : } + 50 : + 51 27 : export const reasoningEnd = ( + 52 7 : state: State, + 53 8 : events: LLMEvent[], + 54 4 : id: string, + 55 21 : providerMetadata?: ProviderMetadata, + 56 3 : ): State => { + 57 44 : if (!state.reasoning.has(id)) return state + 58 0 : const stepped = stepStart(state, events) + 59 0 : events.push(LLMEvent.reasoningEnd({ id, providerMetadata })) + 60 0 : const reasoning = new Set(stepped.reasoning) + 61 0 : reasoning.delete(id) + 62 2 : return { ...stepped, reasoning } + 63 : } + 64 : + 65 0 : export const textEnd = (state: State, events: LLMEvent[], id: string, providerMetadata?: ProviderMetadata): State => { + 66 0 : if (!state.text.has(id)) return state + 67 0 : const stepped = stepStart(state, events) + 68 0 : events.push(LLMEvent.textEnd({ id, providerMetadata })) + 69 0 : const text = new Set(stepped.text) + 70 0 : text.delete(id) + 71 2 : return { ...stepped, text } + 72 : } + 73 : + 74 44 : const closeOpenBlocks = (state: State, events: LLMEvent[]): State => { + 75 38 : for (const id of state.reasoning) events.push(LLMEvent.reasoningEnd({ id })) + 76 70 : for (const id of state.text) events.push(LLMEvent.textEnd({ id })) + 77 56 : return { ...state, text: new Set(), reasoning: new Set() } + 78 : } + 79 : + 80 21 : export const finish = ( + 81 7 : state: State, + 82 8 : events: LLMEvent[], + 83 10 : input: { + 84 : readonly reason: FinishReason + 85 : readonly usage?: Usage + 86 : readonly providerMetadata?: ProviderMetadata + 87 : }, + 88 3 : ): State => { + 89 68 : const stepped = closeOpenBlocks(stepStart(state, events), events) + 90 12 : events.push( + 91 25 : LLMEvent.stepFinish({ + 92 13 : index: 0, + 93 25 : reason: input.reason, + 94 23 : usage: input.usage, + 95 42 : providerMetadata: input.providerMetadata, + 96 4 : }), + 97 22 : LLMEvent.finish(input), + 98 4 : ) + 99 43 : return { ...stepped, stepStarted: false } + 100 : } + 101 : + 102 40 : export * as Lifecycle from "./lifecycle" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Schema } from "effect" + 2 : import type { LLMRequest, ReasoningEffort, TextVerbosity as TextVerbosityValue } from "../../schema" + 3 63 : import { ReasoningEfforts, TextVerbosity } from "../../schema" + 4 : + 5 60 : export const OpenAIReasoningEfforts = ReasoningEfforts.filter( + 6 27 : (effort): effort is Exclude<ReasoningEffort, "max"> => effort !== "max", + 7 3 : ) + 8 : export type OpenAIReasoningEffort = (typeof OpenAIReasoningEfforts)[number] + 9 : + 10 : // Mirrors OpenAI's `ResponseIncludable` union from the official SDK. Keep this + 11 : // in lockstep with `openai-node/src/resources/responses/responses.ts`. + 12 43 : export const OpenAIResponseIncludables = [ + 13 29 : "file_search_call.results", + 14 28 : "web_search_call.results", + 15 35 : "web_search_call.action.sources", + 16 34 : "message.input_image.image_url", + 17 42 : "computer_call_output.output.image_url", + 18 34 : "code_interpreter_call.outputs", + 19 32 : "reasoning.encrypted_content", + 20 31 : "message.output_text.logprobs", + 21 2 : ] as const + 22 : export type OpenAIResponseIncludable = (typeof OpenAIResponseIncludables)[number] + 23 74 : export const OpenAIServiceTiers = ["auto", "default", "flex", "priority"] as const + 24 : export type OpenAIServiceTier = (typeof OpenAIServiceTiers)[number] + 25 : + 26 52 : const REASONING_EFFORTS = new Set<string>(ReasoningEfforts) + 27 65 : const OPENAI_REASONING_EFFORTS = new Set<string>(OpenAIReasoningEfforts) + 28 58 : const TEXT_VERBOSITY = new Set<string>(["low", "medium", "high"]) + 29 55 : const INCLUDABLES = new Set<string>(OpenAIResponseIncludables) + 30 50 : const SERVICE_TIERS = new Set<string>(OpenAIServiceTiers) + 31 : + 32 77 : export const OpenAIReasoningEffort = Schema.Literals(OpenAIReasoningEfforts) + 33 49 : export const OpenAITextVerbosity = TextVerbosity + 34 83 : export const OpenAIResponseIncludable = Schema.Literals(OpenAIResponseIncludables) + 35 69 : export const OpenAIServiceTier = Schema.Literals(OpenAIServiceTiers) + 36 : + 37 39 : const isAnyReasoningEffort = (effort: unknown): effort is ReasoningEffort => + 38 60 : typeof effort === "string" && REASONING_EFFORTS.has(effort) + 39 : + 40 0 : export const isReasoningEffort = (effort: unknown): effort is OpenAIReasoningEffort => + 41 2 : typeof effort === "string" && OPENAI_REASONING_EFFORTS.has(effort) + 42 : + 43 33 : const isTextVerbosity = (value: unknown): value is TextVerbosityValue => + 44 55 : typeof value === "string" && TEXT_VERBOSITY.has(value) + 45 : + 46 59 : const options = (request: LLMRequest) => request.providerOptions?.openai + 47 : + 48 35 : export const store = (request: LLMRequest): boolean | undefined => { + 49 40 : const value = options(request)?.store + 50 53 : return typeof value === "boolean" ? value : undefined + 51 : } + 52 : + 53 45 : export const reasoningEffort = (request: LLMRequest): ReasoningEffort | undefined => { + 54 50 : const value = options(request)?.reasoningEffort + 55 49 : return isAnyReasoningEffort(value) ? value : undefined + 56 : } + 57 : + 58 43 : export const reasoningSummary = (request: LLMRequest): "auto" | undefined => + 59 60 : options(request)?.reasoningSummary === "auto" ? "auto" : undefined + 60 : + 61 : // Resolve the OpenAI Responses `include` field. Filters out unknown + 62 : // includable values defensively so a typo in upstream config drops the + 63 : // invalid entry instead of poisoning the wire body. An empty array (either + 64 : // passed directly or produced by filtering) is treated as "no include" and + 65 : // returns undefined so the request body omits the field entirely. + 66 37 : export const include = (request: LLMRequest): ReadonlyArray<OpenAIResponseIncludable> | undefined => { + 67 42 : const value = options(request)?.include + 68 35 : if (!Array.isArray(value)) return undefined + 69 0 : const filtered = value.filter((entry): entry is OpenAIResponseIncludable => INCLUDABLES.has(entry)) + 70 2 : return filtered.length > 0 ? filtered : undefined + 71 : } + 72 : + 73 44 : export const promptCacheKey = (request: LLMRequest) => { + 74 49 : const value = options(request)?.promptCacheKey + 75 47 : return typeof value === "string" ? value : undefined + 76 : } + 77 : + 78 43 : export const textVerbosity = (request: LLMRequest) => { + 79 48 : const value = options(request)?.textVerbosity + 80 44 : return isTextVerbosity(value) ? value : undefined + 81 : } + 82 : + 83 41 : export const serviceTier = (request: LLMRequest) => { + 84 46 : const value = options(request)?.serviceTier + 85 75 : return typeof value === "string" && SERVICE_TIERS.has(value) ? (value as OpenAIServiceTier) : undefined + 86 : } + 87 : + 88 42 : export const instructions = (request: LLMRequest) => { + 89 47 : const value = options(request)?.instructions + 90 47 : return typeof value === "string" ? value : undefined + 91 : } + 92 : + 93 49 : export * as OpenAIOptions from "./openai-options" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : import type { JsonSchema, ModelToolSchemaCompatibility } from "../../schema" + 2 46 : import { isRecord } from "../../utils/record" + 3 56 : import { GeminiToolSchema } from "./gemini-tool-schema" + 4 : + 5 0 : const removeNullSchemas = (value: unknown): unknown => { + 6 0 : if (Array.isArray(value)) return value.map(removeNullSchemas) + 7 0 : if (!isRecord(value)) return value + 8 0 : const fields = Object.fromEntries( + 9 0 : Object.entries(value) + 10 0 : .filter(([key]) => key !== "anyOf") + 11 0 : .map(([key, field]) => [key, removeNullSchemas(field)]), + 12 0 : ) + 13 0 : if (!Array.isArray(value.anyOf)) return fields + 14 0 : const variants = value.anyOf.filter((variant) => !isRecord(variant) || variant.type !== "null").map(removeNullSchemas) + 15 0 : if (variants.length === 1 && isRecord(variants[0])) return { ...fields, ...variants[0] } + 16 2 : return { ...fields, anyOf: variants } + 17 : } + 18 : + 19 0 : const tupleItemsSchema = (items: ReadonlyArray<unknown>) => { + 20 0 : const projected = items.map(moonshotNode) + 21 0 : if (projected.length === 0) return {} + 22 0 : if (projected.length === 1) return projected[0] + 23 2 : return { anyOf: projected } + 24 : } + 25 : + 26 0 : const moonshotNode = (schema: unknown): unknown => { + 27 0 : if (Array.isArray(schema)) return schema.map(moonshotNode) + 28 0 : if (!isRecord(schema)) return schema + 29 0 : if (typeof schema.$ref === "string") return { $ref: schema.$ref } + 30 0 : return Object.fromEntries( + 31 0 : Object.entries(schema).flatMap(([key, value]) => { + 32 0 : if (key === "items" && Array.isArray(value)) return [[key, tupleItemsSchema(value)]] + 33 0 : if (key === "prefixItems") { + 34 0 : if ("items" in schema) return [] + 35 0 : return [["items", tupleItemsSchema(Array.isArray(value) ? value : [])]] + 36 0 : } + 37 0 : if (key === "unevaluatedItems") return [] + 38 0 : return [[key, moonshotNode(value)]] + 39 0 : }), + 40 2 : ) + 41 : } + 42 : + 43 0 : const moonshot = (schema: JsonSchema): JsonSchema => { + 44 0 : const projected = moonshotNode(schema) + 45 2 : return isRecord(projected) ? projected : {} + 46 : } + 47 : + 48 0 : const openAI = (schema: JsonSchema): JsonSchema => { + 49 0 : const variants = Array.isArray(schema.anyOf) ? schema.anyOf.filter(isRecord) : [] + 50 0 : const flattened = + 51 0 : variants.length === 0 + 52 0 : ? { ...schema, type: "object" } + 53 0 : : { + 54 0 : ...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "anyOf")), + 55 0 : type: "object", + 56 0 : properties: variants.reduce( + 57 0 : (properties, variant) => ({ ...(isRecord(variant.properties) ? variant.properties : {}), ...properties }), + 58 0 : {}, + 59 0 : ), + 60 0 : additionalProperties: false, + 61 0 : } + 62 0 : const normalized = removeNullSchemas(flattened) + 63 2 : return isRecord(normalized) ? normalized : { type: "object" } + 64 : } + 65 : + 66 15 : const gemini = (schema: JsonSchema): JsonSchema => GeminiToolSchema.convert(schema) ?? {} + 67 : + 68 0 : const modelCompatibility = ( + 69 0 : schema: JsonSchema, + 70 0 : compatibility: ModelToolSchemaCompatibility | undefined, + 71 0 : ): JsonSchema => { + 72 0 : if (compatibility === undefined) return schema + 73 0 : switch (compatibility) { + 74 0 : case "gemini": + 75 0 : return gemini(schema) + 76 0 : case "moonshot": + 77 2 : return moonshot(schema) + 78 : } + 79 : } + 80 : + 81 38 : export const ToolSchemaProjection = { + 82 9 : gemini, + 83 21 : modelCompatibility, + 84 11 : moonshot, + 85 7 : openAI, + 86 1 : } as const ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Effect } from "effect" + 2 50 : import { LLMError, LLMEvent, type ProviderMetadata, type ToolCall } from "../../schema" + 3 55 : import { eventError, parseToolInput, type ToolAccumulator } from "../shared" + 4 : + 5 : type StreamKey = string | number + 6 : + 7 : /** + 8 : * One pending streamed tool call. Providers emit the tool identity and JSON + 9 : * argument text across separate chunks; `input` is the raw JSON string collected + 10 : * so far, not the parsed object. + 11 : */ + 12 : export interface PendingTool extends ToolAccumulator { + 13 : readonly providerExecuted?: boolean + 14 : readonly providerMetadata?: ProviderMetadata + 15 : } + 16 : + 17 : /** + 18 : * Sparse parser state keyed by the provider's stream-local tool identifier. + 19 : * + 20 : * This key is not the final tool-call id (`call_...`). It is the id/index the + 21 : * provider uses while streaming a partial call: OpenAI Chat / Anthropic / + 22 : * Bedrock use numeric content indexes, while OpenAI Responses uses string + 23 : * `item_id`s. The generic keeps each protocol internally consistent. + 24 : */ + 25 : export type State<K extends StreamKey> = Partial<Record<K, PendingTool>> + 26 : + 27 : /** + 28 : * Result of adding argument text to one pending tool call. It returns both the + 29 : * next `tools` state and the updated `tool` because parsers often need the + 30 : * current id/name immediately. `events` contains lifecycle and delta events + 31 : * produced by the append; metadata-only deltas update identity without output. + 32 : */ + 33 : export interface AppendOutcome<K extends StreamKey> { + 34 : readonly tools: State<K> + 35 : readonly tool: PendingTool + 36 : readonly events: ReadonlyArray<LLMEvent> + 37 : } + 38 : + 39 : /** Create empty accumulator state for one provider stream. */ + 40 30 : export const empty = <K extends StreamKey>(): State<K> => ({}) + 41 : + 42 0 : const withTool = <K extends StreamKey>(tools: State<K>, key: K, tool: PendingTool): State<K> => { + 43 2 : return { ...tools, [key]: tool } + 44 : } + 45 : + 46 0 : const withoutTool = <K extends StreamKey>(tools: State<K>, key: K): State<K> => { + 47 0 : const next = { ...tools } + 48 0 : delete next[key] + 49 2 : return next + 50 : } + 51 : + 52 0 : const inputStart = (tool: PendingTool) => + 53 0 : LLMEvent.toolInputStart({ + 54 0 : id: tool.id, + 55 0 : name: tool.name, + 56 : providerMetadata: tool.providerMetadata, + 57 2 : }) + 58 : + 59 0 : const inputDelta = (tool: PendingTool, text: string) => + 60 0 : LLMEvent.toolInputDelta({ + 61 0 : id: tool.id, + 62 0 : name: tool.name, + 63 : text, + 64 2 : }) + 65 : + 66 0 : const toolCall = (route: string, tool: PendingTool, inputOverride?: string) => + 67 0 : parseToolInput(route, tool.name, inputOverride ?? tool.input).pipe( + 68 0 : Effect.map( + 69 0 : (input): ToolCall => + 70 0 : LLMEvent.toolCall({ + 71 0 : id: tool.id, + 72 0 : name: tool.name, + 73 0 : input, + 74 0 : providerExecuted: tool.providerExecuted ? true : undefined, + 75 0 : providerMetadata: tool.providerMetadata, + 76 0 : }), + 77 : ), + 78 2 : ) + 79 : + 80 : /** Store the updated tool and produce the optional public delta event. */ + 81 0 : const appendTool = <K extends StreamKey>( + 82 0 : tools: State<K>, + 83 0 : key: K, + 84 0 : tool: PendingTool, + 85 0 : text: string, + 86 0 : ): AppendOutcome<K> => { + 87 0 : const events: LLMEvent[] = [] + 88 0 : if (!tools[key]) events.push(inputStart(tool)) + 89 0 : if (text.length > 0) events.push(inputDelta(tool, text)) + 90 0 : return { + 91 0 : tools: withTool(tools, key, tool), + 92 0 : tool, + 93 0 : events, + 94 2 : } + 95 : } + 96 : + 97 0 : export const isError = <K extends StreamKey>(result: AppendOutcome<K> | LLMError): result is LLMError => + 98 2 : result instanceof LLMError + 99 : + 100 : /** + 101 : * Register a tool call whose start event arrived before any argument deltas. + 102 : * Used by Anthropic `content_block_start`, Bedrock `contentBlockStart`, and + 103 : * OpenAI Responses `response.output_item.added`. + 104 : */ + 105 0 : export const start = <K extends StreamKey>( + 106 0 : tools: State<K>, + 107 0 : key: K, + 108 0 : tool: Omit<PendingTool, "input"> & { readonly input?: string }, + 109 2 : ) => withTool(tools, key, { ...tool, input: tool.input ?? "" }) + 110 : + 111 : /** + 112 : * Append a streamed argument delta, starting the tool if this provider encodes + 113 : * identity on the first delta instead of a separate start event. OpenAI Chat has + 114 : * this shape: `tool_calls[].index` is the stream key, and `id` / `name` may only + 115 : * appear on the first delta for that index. + 116 : */ + 117 0 : export const appendOrStart = <K extends StreamKey>( + 118 0 : route: string, + 119 0 : tools: State<K>, + 120 0 : key: K, + 121 0 : delta: { readonly id?: string; readonly name?: string; readonly text: string }, + 122 0 : missingToolMessage: string, + 123 0 : ): AppendOutcome<K> | LLMError => { + 124 0 : const current = tools[key] + 125 0 : const id = delta.id ?? current?.id + 126 0 : const name = delta.name ?? current?.name + 127 0 : if (!id || !name) return eventError(route, missingToolMessage) + 128 0 : + 129 0 : const tool = { + 130 0 : id, + 131 0 : name, + 132 0 : input: `${current?.input ?? ""}${delta.text}`, + 133 0 : providerExecuted: current?.providerExecuted, + 134 0 : providerMetadata: current?.providerMetadata, + 135 0 : } + 136 0 : if (current && delta.text.length === 0 && current.id === id && current.name === name) + 137 0 : return { tools, tool: current, events: [] } + 138 2 : return appendTool(tools, key, tool, delta.text) + 139 : } + 140 : + 141 : /** + 142 : * Append argument text to a tool that must already have been started. This keeps + 143 : * protocols honest when their stream grammar promises a start event before any + 144 : * argument delta. + 145 : */ + 146 0 : export const appendExisting = <K extends StreamKey>( + 147 0 : route: string, + 148 0 : tools: State<K>, + 149 0 : key: K, + 150 0 : text: string, + 151 0 : missingToolMessage: string, + 152 0 : ): AppendOutcome<K> | LLMError => { + 153 0 : const current = tools[key] + 154 0 : if (!current) return eventError(route, missingToolMessage) + 155 0 : if (text.length === 0) return { tools, tool: current, events: [] } + 156 2 : return appendTool(tools, key, { ...current, input: `${current.input}${text}` }, text) + 157 : } + 158 : + 159 : /** + 160 : * Finalize one pending tool call: parse the accumulated raw JSON, remove it + 161 : * from state, and return the optional public `tool-call` event. Missing keys are + 162 : * a no-op because some providers emit stop events for non-tool content blocks. + 163 : */ + 164 0 : export const finish = <K extends StreamKey>(route: string, tools: State<K>, key: K) => + 165 0 : Effect.gen(function* () { + 166 0 : const tool = tools[key] + 167 0 : if (!tool) return { tools } + 168 0 : return { + 169 0 : tools: withoutTool(tools, key), + 170 0 : events: [ + 171 0 : LLMEvent.toolInputEnd({ id: tool.id, name: tool.name, providerMetadata: tool.providerMetadata }), + 172 0 : yield* toolCall(route, tool), + 173 0 : ], + 174 : } + 175 2 : }) + 176 : + 177 : /** + 178 : * Finalize one pending tool call with an authoritative final input string. + 179 : * OpenAI Responses can send accumulated deltas and then repeat the completed + 180 : * arguments on `response.output_item.done`; the final value wins. + 181 : */ + 182 0 : export const finishWithInput = <K extends StreamKey>(route: string, tools: State<K>, key: K, input: string) => + 183 0 : Effect.gen(function* () { + 184 0 : const tool = tools[key] + 185 0 : if (!tool) return { tools } + 186 0 : return { + 187 0 : tools: withoutTool(tools, key), + 188 0 : events: [ + 189 0 : LLMEvent.toolInputEnd({ id: tool.id, name: tool.name, providerMetadata: tool.providerMetadata }), + 190 0 : yield* toolCall(route, tool, input), + 191 0 : ], + 192 : } + 193 2 : }) + 194 : + 195 : /** + 196 : * Finalize every pending tool call at once. OpenAI Chat has this shape: it does + 197 : * not emit per-tool stop events, so all accumulated calls finish when the choice + 198 : * receives a terminal `finish_reason`. + 199 : */ + 200 0 : export const finishAll = <K extends StreamKey>(route: string, tools: State<K>) => + 201 0 : Effect.gen(function* () { + 202 0 : const pending = Object.values<PendingTool | undefined>(tools).filter( + 203 0 : (tool): tool is PendingTool => tool !== undefined, + 204 0 : ) + 205 0 : return { + 206 0 : tools: empty<K>(), + 207 0 : events: yield* Effect.forEach(pending, (tool) => + 208 0 : toolCall(route, tool).pipe( + 209 0 : Effect.map((call) => [ + 210 0 : LLMEvent.toolInputEnd({ id: tool.id, name: tool.name, providerMetadata: tool.providerMetadata }), + 211 0 : call, + 212 0 : ]), + 213 0 : ), + 214 0 : ).pipe(Effect.map((events) => events.flat())), + 215 : } + 216 2 : }) + 217 : + 218 43 : export * as ToolStream from "./tool-stream" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Schema } from "effect" + 2 56 : import { LLMError, ProviderErrorEvent } from "./schema" + 3 : + 4 19 : const patterns = [ + 5 24 : /prompt is too long/i, + 6 23 : /request_too_large/i, + 7 43 : /input is too long for requested model/i, + 8 32 : /exceeds the context window/i, + 9 95 : /exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i, + 10 44 : /input token count.*exceeds the maximum/i, + 11 52 : /tokens in request more than max tokens allowed/i, + 12 34 : /maximum prompt length is \d+/i, + 13 39 : /reduce the length of the messages/i, + 14 42 : /maximum context length is \d+ tokens/i, + 15 69 : /exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i, + 16 84 : /input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i, + 17 30 : /exceeds the limit of \d+/i, + 18 40 : /exceeds the available context size/i, + 19 37 : /greater than the context length/i, + 20 34 : /context window exceeds limit/i, + 21 32 : /exceeded model token limit/i, + 22 35 : /context[_ ]length[_ ]exceeded/i, + 23 30 : /request entity too large/i, + 24 39 : /context length is only \d+ tokens/i, + 25 43 : /input length.*exceeds.*context length/i, + 26 55 : /prompt too long; exceeded (?:max )?context length/i, + 27 57 : /too large for model with \d+ maximum context length/i, + 28 82 : /prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i, + 29 35 : /model_context_window_exceeded/i, + 30 21 : /too many tokens/i, + 31 24 : /token limit exceeded/i, + 32 2 : ] + 33 : + 34 102 : const exclusions = [/^(throttling error|service unavailable):/i, /rate limit/i, /too many requests/i] + 35 : + 36 0 : export const isContextOverflow = (message: string) => + 37 0 : !exclusions.some((pattern) => pattern.test(message)) && + 38 2 : (patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)) + 39 : + 40 51 : export const isContextOverflowFailure = (failure: unknown) => + 41 29 : failure instanceof LLMError + 42 98 : ? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow" + 43 88 : : Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : import type { Model, ModelID, ProviderID } from "./schema" + 2 : + 3 : export type ModelOptions = Pick<Model.Input, "defaults" | "compatibility"> + 4 : + 5 : /** + 6 : * Advanced structural provider definition helper. Built-in providers should + 7 : * prefer explicit `configure(options).model(id)` facades so deployment config is + 8 : * chosen before model selection. The optional `apis` map remains for external + 9 : * structural providers that expose multiple route selectors behind one provider. + 10 : */ + 11 : export type ModelFactory<Options extends ModelOptions = ModelOptions> = ( + 12 : id: string | ModelID, + 13 : options?: Options, + 14 : ) => Model + 15 : + 16 : type AnyModelFactory = (...args: never[]) => Model + 17 : + 18 : export interface Definition<Factory extends AnyModelFactory = ModelFactory> { + 19 : readonly id: ProviderID + 20 : readonly model: Factory + 21 : readonly apis?: Record<string, AnyModelFactory> + 22 : } + 23 : + 24 : type DefinitionShape = { + 25 : readonly id: ProviderID + 26 : readonly model: (...args: never[]) => Model + 27 : readonly apis?: Record<string, (...args: never[]) => Model> + 28 : } + 29 : + 30 : type NoExtraFields<Input, Shape> = Input & Record<Exclude<keyof Input, keyof Shape>, never> + 31 : + 32 0 : export const make = <DefinitionType extends DefinitionShape>( + 33 0 : definition: NoExtraFields<DefinitionType, DefinitionShape>, + 34 2 : ) => definition + 35 : + 36 38 : export * as Provider from "./provider" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : import type { Config, Redacted } from "effect" + 2 30 : import { Auth } from "./auth" + 3 : + 4 : export type ApiKeyMode = "optional" | "required" + 5 : + 6 : export type AuthOverride = { + 7 : readonly auth: Auth + 8 : readonly apiKey?: never + 9 : } + 10 : + 11 : export type OptionalApiKeyAuth = { + 12 : readonly apiKey?: string | Redacted.Redacted<string> | Config.Config<string | Redacted.Redacted<string>> + 13 : readonly auth?: never + 14 : } + 15 : + 16 : export type RequiredApiKeyAuth = { + 17 : readonly apiKey: string | Redacted.Redacted<string> | Config.Config<string | Redacted.Redacted<string>> + 18 : readonly auth?: never + 19 : } + 20 : + 21 : export type ProviderAuthOption<Mode extends ApiKeyMode> = + 22 : | AuthOverride + 23 : | (Mode extends "optional" ? OptionalApiKeyAuth : RequiredApiKeyAuth) + 24 : + 25 : export type ModelOptions<Base, Mode extends ApiKeyMode> = Omit<Base, "apiKey" | "auth"> & ProviderAuthOption<Mode> + 26 : + 27 : export type ModelArgs<Base, Mode extends ApiKeyMode> = Mode extends "optional" + 28 : ? readonly [options?: ModelOptions<Base, Mode>] + 29 : : readonly [options: ModelOptions<Base, Mode>] + 30 : + 31 : export type ModelFactory<Base, Mode extends ApiKeyMode, Model> = (id: string, ...args: ModelArgs<Base, Mode>) => Model + 32 : + 33 : /** + 34 : * Require at least one of the keys in `T`. Use for option shapes where any + 35 : * subset of fields is acceptable but at least one must be present (e.g. Azure + 36 : * accepts `resourceName` or `baseURL`). + 37 : */ + 38 : export type AtLeastOne<T> = { + 39 : [K in keyof T]: Required<Pick<T, K>> & Partial<Omit<T, K>> + 40 : }[keyof T] + 41 : + 42 : /** + 43 : * Standard bearer-auth resolution for providers: honor an explicit `auth` + 44 : * override, otherwise resolve `apiKey` (option > config var) and apply it as + 45 : * a bearer token. + 46 : */ + 47 0 : export const bearer = (options: ProviderAuthOption<"optional">, envVar: string | ReadonlyArray<string>): Auth => { + 48 0 : if ("auth" in options && options.auth) return options.auth + 49 0 : return (Array.isArray(envVar) ? envVar : [envVar]) + 50 0 : .reduce( + 51 0 : (auth, name) => auth.orElse(Auth.config(name)), + 52 0 : Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey"), + 53 0 : ) + 54 2 : .bearer() + 55 : } + 56 : + 57 45 : export * as AuthOptions from "./auth-options" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : import { Config, Effect, Redacted } from "effect" + 2 : import { Headers } from "effect/unstable/http" + 3 : import { AuthenticationReason, InvalidRequestReason, LLMError, type LLMRequest } from "../schema" + 4 : + 5 : export class MissingCredentialError extends Error { + 6 1 : readonly _tag = "MissingCredentialError" + 7 : + 8 0 : constructor(readonly source: string) { + 9 48 : super(`Missing auth credential: ${source}`) + 10 : } + 11 2 : } + 12 : + 13 : export type CredentialError = MissingCredentialError | Config.ConfigError + 14 : export type AuthError = CredentialError | LLMError + 15 : type Secret = string | Redacted.Redacted | Config.Config<string | Redacted.Redacted> + 16 : + 17 : export interface AuthInput { + 18 : readonly request: LLMRequest + 19 : readonly method: "POST" | "GET" + 20 : readonly url: string + 21 : readonly body: string + 22 : readonly headers: Headers.Headers + 23 : } + 24 : + 25 : export interface Credential { + 26 : readonly load: Effect.Effect<Redacted.Redacted, CredentialError> + 27 : readonly orElse: (that: Credential) => Credential + 28 : readonly bearer: () => Auth + 29 : readonly header: (name: string) => Auth + 30 : readonly pipe: <A>(f: (self: Credential) => A) => A + 31 : } + 32 : + 33 : export interface Auth { + 34 : readonly apply: (input: AuthInput) => Effect.Effect<Headers.Headers, AuthError> + 35 : readonly andThen: (that: Auth) => Auth + 36 : readonly orElse: (that: Auth) => Auth + 37 : readonly pipe: <A>(f: (self: Auth) => A) => A + 38 : } + 39 : + 40 0 : export const isAuth = (input: unknown): input is Auth => + 41 106 : typeof input === "object" && input !== null && "apply" in input && typeof input.apply === "function" + 42 : + 43 61 : const credential = (load: Effect.Effect<Redacted.Redacted, CredentialError>): Credential => { + 44 36 : const self: Credential = { + 45 18 : load, + 46 87 : orElse: (that) => credential(load.pipe(Effect.catch(() => that.load))), + 47 180 : bearer: () => fromCredential(self, (secret) => ({ authorization: `Bearer ${secret}` })), + 48 119 : header: (name) => fromCredential(self, (secret) => ({ [name]: secret })), + 49 29 : pipe: (f) => f(self), + 50 8 : } + 51 28 : return self + 52 : } + 53 : + 54 51 : const auth = (apply: Auth["apply"]): Auth => { + 55 36 : const self: Auth = { + 56 20 : apply, + 57 0 : andThen: (that) => + 58 108 : auth((input) => apply(input).pipe(Effect.flatMap((headers) => that.apply({ ...input, headers })))), + 59 108 : orElse: (that) => auth((input) => apply(input).pipe(Effect.catch(() => that.apply(input)))), + 60 29 : pipe: (f) => f(self), + 61 8 : } + 62 28 : return self + 63 : } + 64 : + 65 83 : const fromCredential = (source: Credential, render: (secret: string) => Headers.Input) => + 66 31 : auth((input) => + 67 203 : source.load.pipe(Effect.map((secret) => Headers.setAll(input.headers, render(Redacted.value(secret))))), + 68 5 : ) + 69 : + 70 85 : const secretEffect = (secret: string | Redacted.Redacted, source: string) => { + 71 147 : const redacted = typeof secret === "string" ? Redacted.make(secret) : secret + 72 138 : if (Redacted.value(redacted) === "") return Effect.fail(new MissingCredentialError(source)) + 73 68 : return Effect.succeed(redacted) + 74 : } + 75 : + 76 101 : const credentialFromSecret = (secret: Secret, source: string) => { + 77 228 : if (typeof secret === "string" || Redacted.isRedacted(secret)) return credential(secretEffect(secret, source)) + 78 18 : return credential( + 79 0 : Effect.gen(function* () { + 80 53 : return yield* secretEffect(yield* secret, source) + 81 1 : }), + 82 7 : ) + 83 : } + 84 : + 85 174 : export const value = (secret: string, source = "value") => credentialFromSecret(secret, source) + 86 : + 87 0 : export const optional = (secret: Secret | undefined, source = "optional value") => + 88 0 : secret === undefined + 89 0 : ? credential(Effect.fail(new MissingCredentialError(source))) + 90 40 : : credentialFromSecret(secret, source) + 91 : + 92 104 : export const config = (name: string) => credentialFromSecret(Config.redacted(name), name) + 93 : + 94 71 : export const effect = (load: Effect.Effect<Redacted.Redacted, CredentialError>) => credential(load) + 95 : + 96 93 : export const none = auth((input) => Effect.succeed(input.headers)) + 97 : + 98 0 : export const headers = (input: Headers.Input) => + 99 81 : auth((inputAuth) => Effect.succeed(Headers.setAll(inputAuth.headers, input))) + 100 : + 101 123 : export const remove = (name: string) => auth((input) => Effect.succeed(Headers.remove(input.headers, name))) + 102 : + 103 67 : export const custom = (apply: (input: AuthInput) => Effect.Effect<Headers.Headers, LLMError>) => auth(apply) + 104 : + 105 64 : export const passthrough = none + 106 : + 107 69 : const credentialInput = (source: Secret | Credential) => + 108 173 : typeof source === "string" || Redacted.isRedacted(source) || Config.isConfig(source) + 109 79 : ? credentialFromSecret(source, "value") + 110 16 : : source + 111 : + 112 : export function bearer(source: Secret | Credential): Auth + 113 50 : export function bearer(source: Secret | Credential) { + 114 82 : return credentialInput(source).bearer() + 115 : } + 116 : + 117 58 : export const apiKey = bearer + 118 : + 119 : export function header(name: string): (source: Secret | Credential) => Auth + 120 : export function header(name: string, source: Secret | Credential): Auth + 121 62 : export function header(name: string, source?: Secret | Credential) { + 122 56 : if (source === undefined) { + 123 54 : return (next: Secret | Credential) => credentialInput(next).header(name) + 124 5 : } + 125 90 : return credentialInput(source).header(name) + 126 : } + 127 : + 128 : export function bearerHeader(name: string): (source: Secret | Credential) => Auth + 129 : export function bearerHeader(name: string, source: Secret | Credential): Auth + 130 0 : export function bearerHeader(name: string, source?: Secret | Credential) { + 131 0 : const render = (input: Secret | Credential) => + 132 0 : fromCredential(credentialInput(input), (secret) => ({ [name]: `Bearer ${secret}` })) + 133 0 : if (source === undefined) return render + 134 25 : return render(source) + 135 : } + 136 : + 137 0 : const toLLMError = (error: AuthError): LLMError => { + 138 0 : if (error instanceof MissingCredentialError || error instanceof Config.ConfigError) { + 139 0 : return new LLMError({ + 140 0 : module: "Auth", + 141 0 : method: "apply", + 142 0 : reason: + 143 0 : error instanceof MissingCredentialError + 144 0 : ? new AuthenticationReason({ message: error.message, kind: "missing" }) + 145 0 : : new InvalidRequestReason({ message: `Failed to resolve auth config: ${error.message}` }), + 146 0 : }) + 147 0 : } + 148 18 : return error + 149 : } + 150 : + 151 45 : export const toEffect = + 152 21 : (input: Auth) => + 153 30 : (authInput: AuthInput): Effect.Effect<Headers.Headers, LLMError> => + 154 116 : input.apply(authInput).pipe(Effect.mapError(toLLMError)) + 155 : + 156 61 : export * as Auth from "./auth" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 33 : import { Cause, Context, Effect, Layer, Schema, Stream } from "effect" + 2 40 : import * as Option from "effect/Option" + 3 30 : import { Auth, type Auth as AuthDef } from "./auth" + 4 38 : import { Endpoint, type EndpointPatch } from "./endpoint" + 5 45 : import { RequestExecutor } from "./executor" + 6 : import type { Framing } from "./framing" + 7 44 : import { HttpTransport } from "./transport" + 8 : import type { Transport, TransportRuntime } from "./transport" + 9 48 : import { WebSocketExecutor } from "./transport" + 10 : import type { Protocol } from "./protocol" + 11 51 : import { applyCachePolicy } from "../cache-policy" + 12 54 : import * as ProviderShared from "../protocols/shared" + 13 : import type { LLMError, LLMEvent, PreparedRequestOf, ProtocolID, ProviderOptions } from "../schema" + 14 104 : import { + 15 : GenerationOptions, + 16 : HttpOptions, + 17 : LLMRequest, + 18 : LLMResponse, + 19 : Model, + 20 : ModelLimits, + 21 131 : LLMError as LLMErrorClass, + 22 : PreparedRequest, + 23 : ProviderID, + 24 : mergeGenerationOptions, + 25 : mergeHttpOptions, + 26 : mergeProviderOptions, + 27 : } from "../schema" + 28 : + 29 : export interface RouteBody<Body> { + 30 : /** Schema for the validated provider-native body sent as the JSON request. */ + 31 : readonly schema: Schema.Codec<Body, unknown> + 32 : /** Build the provider-native body from a common `LLMRequest`. */ + 33 : readonly from: (request: LLMRequest) => Effect.Effect<Body, LLMError> + 34 : } + 35 : + 36 : export interface Route<Body, Prepared = unknown> { + 37 : readonly id: string + 38 : readonly provider?: ProviderID + 39 : readonly protocol: ProtocolID + 40 : readonly endpoint: Endpoint<Body> + 41 : readonly auth: AuthDef + 42 : readonly transport: Transport<Body, Prepared, unknown> + 43 : readonly defaults: RouteDefaults + 44 : readonly body: RouteBody<Body> + 45 : readonly with: (patch: RoutePatch<Body, Prepared>) => Route<Body, Prepared> + 46 : readonly model: (input: RouteMappedModelInput) => Model + 47 : readonly prepareTransport: (body: Body, request: LLMRequest) => Effect.Effect<Prepared, LLMError> + 48 : readonly streamPrepared: ( + 49 : prepared: Prepared, + 50 : request: LLMRequest, + 51 : runtime: TransportRuntime, + 52 : ) => Stream.Stream<LLMEvent, LLMError> + 53 : } + 54 : + 55 : // Route registries intentionally erase body generics after construction. + 56 : // Normal call sites use `OpenAIChat.route`; callers only need body types + 57 : // when preparing a request with a protocol-specific type assertion. + 58 : // oxlint-disable-next-line typescript-eslint/no-explicit-any + 59 : export type AnyRoute = Route<any, any> + 60 : + 61 : export type HttpOptionsInput = HttpOptions.Input + 62 : + 63 : export type RouteModelInput = Omit<Model.Input, "provider" | "route"> + 64 : + 65 : export type RouteRoutedModelInput = Omit<Model.Input, "route"> + 66 : + 67 : export interface RouteDefaults { + 68 : readonly headers?: Record<string, string> + 69 : readonly limits?: ModelLimits + 70 : readonly generation?: GenerationOptions + 71 : readonly providerOptions?: ProviderOptions + 72 : readonly http?: HttpOptions + 73 : } + 74 : + 75 : export interface RouteDefaultsInput { + 76 : readonly headers?: Record<string, string> + 77 : readonly limits?: ModelLimits.Input + 78 : readonly generation?: GenerationOptions.Input + 79 : readonly providerOptions?: ProviderOptions + 80 : readonly http?: HttpOptions.Input + 81 : } + 82 : + 83 : export interface RoutePatch<Body, Prepared> extends RouteDefaultsInput { + 84 : readonly id?: string + 85 : readonly provider?: string | ProviderID + 86 : readonly auth?: AuthDef + 87 : readonly transport?: Transport<Body, Prepared, unknown> + 88 : readonly endpoint?: EndpointPatch<Body> + 89 : } + 90 : + 91 : type RouteMappedModelInput = RouteModelInput | RouteRoutedModelInput + 92 : + 93 43 : const makeRouteModel = (route: AnyRoute, mapped: RouteMappedModelInput) => { + 94 58 : const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined) + 95 2 : if (!provider) throw new Error(`Route.model(${route.id}) requires a provider`) + 96 40 : if (!endpointBaseURL(route.endpoint)) + 97 2 : throw new Error(`Route.model(${route.id}) requires an endpoint baseURL — configure it on the route first`) + 98 26 : return Model.make({ + 99 11 : ...mapped, + 100 13 : provider, + 101 7 : route, + 102 4 : }) + 103 : } + 104 : + 105 45 : const mergeRouteDefaults = (base: RouteDefaults | undefined, patch: RouteDefaultsInput): RouteDefaults => { + 106 61 : const headers = mergeHeaders(base?.headers, patch.headers) + 107 15 : return { + 108 12 : ...base, + 109 10 : ...patch, + 110 12 : headers, + 111 84 : limits: patch.limits === undefined ? base?.limits : ModelLimits.make(patch.limits), + 112 113 : generation: mergeGenerationOptions(generationOptions(base?.generation), generationOptions(patch.generation)), + 113 88 : providerOptions: mergeProviderOptions(base?.providerOptions, patch.providerOptions), + 114 23 : http: mergeHttpOptions( + 115 12 : base?.http, + 116 25 : httpOptions(patch.http), + 117 62 : headers === undefined ? undefined : new HttpOptions({ headers }), + 118 2 : ), + 119 3 : } + 120 : } + 121 : + 122 36 : const endpointBaseURL = <Body>(endpoint: Endpoint<Body>) => + 123 56 : typeof endpoint.baseURL === "string" ? endpoint.baseURL : undefined + 124 : + 125 36 : const mergeHeaders = (...items: ReadonlyArray<Record<string, string> | undefined>) => { + 126 39 : const entries = items.flatMap((item) => + 127 84 : item === undefined ? [] : Object.entries(item).filter((entry): entry is [string, string] => entry[1] !== undefined), + 128 4 : ) + 129 36 : if (entries.length === 0) return undefined + 130 36 : return Object.fromEntries(entries) + 131 : } + 132 : + 133 42 : export const generationOptions = (input: GenerationOptions.Input | undefined) => + 134 63 : input === undefined ? undefined : GenerationOptions.make(input) + 135 : + 136 39 : export const httpOptions = (input: HttpOptionsInput | undefined) => { + 137 41 : if (input === undefined) return input + 138 32 : return HttpOptions.make(input) + 139 : } + 140 : + 141 : export interface Interface { + 142 : /** + 143 : * Compile a request through protocol body construction, validation, and HTTP + 144 : * preparation without sending it. Returns the prepared request including the + 145 : * provider-native body. + 146 : * + 147 : * Pass a `Body` type argument to statically expose the route's body + 148 : * shape (e.g. `prepare<OpenAIChatBody>(...)`) — the runtime body is + 149 : * identical, so this is a type-level assertion the caller makes about which + 150 : * route the request will resolve to. + 151 : */ + 152 : readonly prepare: <Body = unknown>(request: LLMRequest) => Effect.Effect<PreparedRequestOf<Body>, LLMError> + 153 : readonly stream: StreamMethod + 154 : readonly generate: GenerateMethod + 155 : } + 156 : + 157 : export interface StreamMethod { + 158 : (request: LLMRequest): Stream.Stream<LLMEvent, LLMError> + 159 : } + 160 : + 161 : export interface GenerateMethod { + 162 : (request: LLMRequest): Effect.Effect<LLMResponse, LLMError> + 163 : } + 164 : + 165 72 : export class Service extends Context.Service<Service, Interface>()("@opencode/LLMClient") {} + 166 : + 167 44 : const resolveRequestOptions = (request: LLMRequest) => { + 168 53 : const routeDefaults = request.model.route.defaults + 169 47 : const modelDefaults = request.model.defaults + 170 117 : const generation = mergeGenerationOptions(routeDefaults.generation, modelDefaults?.generation, request.generation) + 171 39 : return LLMRequest.update(request, { + 172 56 : generation: generation ?? new GenerationOptions({}), + 173 38 : providerOptions: mergeProviderOptions( + 174 31 : routeDefaults.providerOptions, + 175 32 : modelDefaults?.providerOptions, + 176 23 : request.providerOptions, + 177 6 : ), + 178 79 : http: mergeHttpOptions(routeDefaults.http, modelDefaults?.http, request.http), + 179 4 : }) + 180 : } + 181 : + 182 : export interface MakeInput<Body, Frame, Event, State> { + 183 : /** Route id used in diagnostics and prepared request metadata. */ + 184 : readonly id: string + 185 : /** Provider identity for route-owned model construction. */ + 186 : readonly provider?: string | ProviderID + 187 : /** Semantic API contract — owns body construction, body schema, and parsing. */ + 188 : readonly protocol: Protocol<Body, Frame, Event, State> + 189 : /** Where the request is sent. */ + 190 : readonly endpoint: Endpoint<Body> + 191 : /** Per-request transport auth. Provider facades override this via `route.with(...)`. */ + 192 : readonly auth?: AuthDef + 193 : /** Stream framing — bytes -> frames before `protocol.stream.event` decoding. */ + 194 : readonly framing: Framing<Frame> + 195 : /** Static / per-request headers added before `auth` runs. */ + 196 : readonly headers?: (input: { readonly request: LLMRequest }) => Record<string, string> + 197 : /** Route/request defaults used when compiling requests for this route. */ + 198 : readonly defaults?: RouteDefaultsInput + 199 : } + 200 : + 201 : export interface MakeTransportInput<Body, Prepared, Frame, Event, State> { + 202 : /** Route id used in diagnostics and prepared request metadata. */ + 203 : readonly id: string + 204 : /** Provider identity for route-owned model construction. */ + 205 : readonly provider?: string | ProviderID + 206 : /** Semantic API contract — owns body construction, body schema, and parsing. */ + 207 : readonly protocol: Protocol<Body, Frame, Event, State> + 208 : /** Where the request is sent. */ + 209 : readonly endpoint: Endpoint<Body> + 210 : /** Per-request transport auth. Provider facades override this via `route.with(...)`. */ + 211 : readonly auth?: AuthDef + 212 : /** Static / per-request headers added before `auth` runs. */ + 213 : readonly headers?: (input: { readonly request: LLMRequest }) => Record<string, string> + 214 : /** Runnable transport route. */ + 215 : readonly transport: Transport<Body, Prepared, Frame> + 216 : /** Route/request defaults used when compiling requests for this route. */ + 217 : readonly defaults?: RouteDefaultsInput + 218 : } + 219 : + 220 0 : const streamError = (route: string, message: string, cause: Cause.Cause<unknown>) => { + 221 0 : const failed = cause.reasons.find(Cause.isFailReason)?.error + 222 0 : if (failed instanceof LLMErrorClass) return failed + 223 1 : return ProviderShared.eventError(route, message, Cause.pretty(cause)) + 224 : } + 225 : + 226 1 : function makeFromTransport<Body, Prepared, Frame, Event, State>( + 227 7 : input: MakeTransportInput<Body, Prepared, Frame, Event, State>, + 228 3 : ): Route<Body, Prepared> { + 229 34 : const protocol = input.protocol + 230 84 : const encodeBody = Schema.encodeSync(Schema.fromJsonString(protocol.body.schema)) + 231 78 : const decodeEventEffect = Schema.decodeUnknownEffect(protocol.stream.event) + 232 40 : const decodeEvent = (route: string) => (frame: Frame) => + 233 30 : decodeEventEffect(frame).pipe( + 234 0 : Effect.mapError(() => + 235 0 : ProviderShared.eventError( + 236 0 : input.id, + 237 0 : `Invalid ${route} stream event`, + 238 : typeof frame === "string" ? frame : ProviderShared.encodeJson(frame), + 239 : ), + 240 1 : ), + 241 4 : ) + 242 : + 243 : type BuiltRouteInput = Omit<MakeTransportInput<Body, Prepared, Frame, Event, State>, "defaults"> & { + 244 : readonly defaults?: RouteDefaults + 245 : } + 246 : + 247 34 : const build = (routeInput: BuiltRouteInput): Route<Body, Prepared> => { + 248 21 : const route: Route<Body, Prepared> = { + 249 24 : id: routeInput.id, + 250 98 : provider: routeInput.provider === undefined ? undefined : ProviderID.make(routeInput.provider), + 251 28 : protocol: protocol.id, + 252 36 : endpoint: routeInput.endpoint, + 253 41 : auth: routeInput.auth ?? Auth.none, + 254 38 : transport: routeInput.transport, + 255 42 : defaults: routeInput.defaults ?? {}, + 256 26 : body: protocol.body, + 257 25 : with: (patch: RoutePatch<Body, Prepared>) => { + 258 79 : const { id, provider, auth, transport, endpoint, ...defaults } = patch + 259 27 : return build({ + 260 21 : ...routeInput, + 261 34 : id: id ?? routeInput.id, + 262 52 : provider: provider ?? routeInput.provider, + 263 40 : auth: auth ?? routeInput.auth, + 264 96 : endpoint: endpoint ? Endpoint.merge(routeInput.endpoint, endpoint) : routeInput.endpoint, + 265 55 : transport: (transport as Transport<Body, Prepared, Frame> | undefined) ?? routeInput.transport, + 266 62 : defaults: mergeRouteDefaults(route.defaults, defaults), + 267 9 : }) + 268 : }, + 269 51 : model: (input) => makeRouteModel(route, input), + 270 36 : prepareTransport: (body, request) => + 271 38 : routeInput.transport.prepare({ + 272 13 : body, + 273 16 : request, + 274 38 : endpoint: routeInput.endpoint, + 275 43 : auth: routeInput.auth ?? Auth.none, + 276 19 : encodeBody, + 277 33 : headers: routeInput.headers, + 278 8 : }), + 279 56 : streamPrepared: (prepared: Prepared, request: LLMRequest, runtime: TransportRuntime) => { + 280 76 : const route = `${request.model.provider}/${request.model.route.id}` + 281 36 : const events = routeInput.transport + 282 35 : .frames(prepared, request, runtime) + 283 5 : .pipe( + 284 38 : Stream.mapEffect(decodeEvent(route)), + 285 44 : protocol.stream.terminal ? Stream.takeUntil(protocol.stream.terminal) : (stream) => stream, + 286 9 : ) + 287 19 : return events.pipe( + 288 21 : Stream.mapAccumEffect( + 289 39 : () => protocol.stream.initial(request), + 290 22 : protocol.stream.step, + 291 58 : protocol.stream.onHalt ? { onHalt: protocol.stream.onHalt } : undefined, + 292 2 : ), + 293 18 : Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), + 294 5 : ) + 295 : }, + 296 6 : } satisfies Route<Body, Prepared> + 297 15 : return route + 298 : } + 299 : + 300 90 : return build({ ...input, defaults: mergeRouteDefaults(undefined, input.defaults ?? {}) }) + 301 : } + 302 : + 303 : export function make<Body, Prepared, Frame, Event, State>( + 304 : input: MakeTransportInput<Body, Prepared, Frame, Event, State>, + 305 : ): Route<Body, Prepared> + 306 : /** + 307 : * Build a `Route` by composing the four orthogonal pieces of a deployment: + 308 : * + 309 : * - `Protocol` — what is the API I'm speaking? + 310 : * - `Endpoint` — where do I send the request? + 311 : * - `Auth` — how do I authenticate it? + 312 : * - `Framing` — how do I cut the response stream into protocol frames? + 313 : * + 314 : * Plus optional `headers` for cross-cutting deployment concerns (provider + 315 : * version pins, per-deployment quirks). + 316 : * + 317 : * This is the canonical route constructor. If a new route does not fit + 318 : * this four-axis model, add a purpose-built constructor rather than widening + 319 : * the public surface preemptively. + 320 : */ + 321 : export function make<Body, Frame, Event, State>( + 322 : input: MakeInput<Body, Frame, Event, State>, + 323 : ): Route<Body, HttpTransport.HttpPrepared<Frame>> + 324 6 : export function make<Body, Prepared, Frame, Event, State>( + 325 7 : input: MakeInput<Body, Frame, Event, State> | MakeTransportInput<Body, Prepared, Frame, Event, State>, + 326 3 : ): Route<Body, Prepared> | Route<Body, HttpTransport.HttpPrepared<Frame>> { + 327 61 : if ("transport" in input) return makeFromTransport(input) + 328 34 : const protocol = input.protocol + 329 30 : return makeFromTransport({ + 330 17 : id: input.id, + 331 29 : provider: input.provider, + 332 13 : protocol, + 333 29 : endpoint: input.endpoint, + 334 21 : auth: input.auth, + 335 27 : headers: input.headers, + 336 66 : transport: HttpTransport.httpJson({ framing: input.framing }), + 337 26 : defaults: input.defaults, + 338 3 : }) + 339 : } + 340 : + 341 : // `compile` is the important boundary: it turns a common `LLMRequest` into a + 342 : // validated provider body plus transport-private prepared data, but does not + 343 : // execute transport. + 344 51 : const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) { + 345 68 : const resolved = applyCachePolicy(resolveRequestOptions(request)) + 346 37 : const route = resolved.model.route + 347 : + 348 31 : const body = yield* route.body + 349 15 : .from(resolved) + 350 99 : .pipe(Effect.flatMap(ProviderShared.validateWith(Schema.decodeUnknownEffect(route.body.schema)))) + 351 65 : const prepared = yield* route.prepareTransport(body, resolved) + 352 : + 353 12 : return { + 354 22 : request: resolved, + 355 10 : route, + 356 9 : body, + 357 10 : prepared, + 358 1 : } + 359 3 : }) + 360 : + 361 61 : const prepareWith = Effect.fn("LLMClient.prepare")(function* (request: LLMRequest) { + 362 43 : const compiled = yield* compile(request) + 363 : + 364 32 : return new PreparedRequest({ + 365 41 : id: compiled.request.id ?? "request", + 366 29 : route: compiled.route.id, + 367 38 : protocol: compiled.route.protocol, + 368 34 : model: compiled.request.model, + 369 24 : body: compiled.body, + 370 54 : metadata: { transport: compiled.route.transport.id }, + 371 2 : }) + 372 3 : }) + 373 : + 374 49 : const streamRequestWith = (runtime: TransportRuntime) => (request: LLMRequest) => + 375 14 : Stream.unwrap( + 376 15 : Effect.gen(function* () { + 377 43 : const compiled = yield* compile(request) + 378 82 : return compiled.route.streamPrepared(compiled.prepared, compiled.request, runtime) + 379 1 : }), + 380 3 : ) + 381 : + 382 31 : const generateWith = (stream: Interface["stream"]) => + 383 0 : Effect.fn("LLM.generate")(function* (request: LLMRequest) { + 384 0 : const state = yield* stream(request).pipe(Stream.runFold(LLMResponse.empty, LLMResponse.reduce)) + 385 0 : const response = LLMResponse.complete(state) + 386 0 : if (response) return response + 387 0 : return yield* ProviderShared.eventError( + 388 0 : `${request.model.provider}/${request.model.route.id}`, + 389 0 : "Provider stream ended without a terminal finish event", + 390 : ) + 391 2 : }) + 392 : + 393 34 : export const prepare = <Body = unknown>(request: LLMRequest) => + 394 21 : prepareWith(request) as Effect.Effect<PreparedRequestOf<Body>, LLMError> + 395 : + 396 0 : export function stream(request: LLMRequest): Stream.Stream<LLMEvent, LLMError> { + 397 0 : return Stream.unwrap( + 398 0 : Effect.gen(function* () { + 399 0 : return (yield* Service).stream(request) + 400 0 : }), + 401 1 : ) as Stream.Stream<LLMEvent, LLMError> + 402 : } + 403 : + 404 0 : export function generate(request: LLMRequest): Effect.Effect<LLMResponse, LLMError> { + 405 0 : return Effect.gen(function* () { + 406 0 : return yield* (yield* Service).generate(request) + 407 1 : }) as Effect.Effect<LLMResponse, LLMError> + 408 : } + 409 : + 410 0 : export const streamRequest = (request: LLMRequest) => + 411 0 : Stream.unwrap( + 412 0 : Effect.gen(function* () { + 413 0 : return (yield* Service).stream(request) + 414 : }), + 415 2 : ) + 416 : + 417 33 : export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer.effect( + 418 9 : Service, + 419 15 : Effect.gen(function* () { + 420 38 : const stream = streamRequestWith({ + 421 41 : http: yield* RequestExecutor.Service, + 422 90 : webSocket: Option.getOrUndefined(yield* Effect.serviceOption(WebSocketExecutor.Service)), + 423 5 : }) + 424 83 : return Service.of({ prepare: prepareWith as Interface["prepare"], stream, generate: generateWith(stream) }) + 425 1 : }), + 426 3 : ) + 427 : + 428 30 : export const Route = { make } as const + 429 : + 430 27 : export const LLMClient = { + 431 10 : Service, + 432 8 : layer, + 433 10 : prepare, + 434 9 : stream, + 435 9 : generate, + 436 1 : } as const ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : import type { LLMRequest } from "../schema" + 2 54 : import * as ProviderShared from "../protocols/shared" + 3 : + 4 : export interface EndpointInput<Body> { + 5 : readonly request: LLMRequest + 6 : readonly body: Body + 7 : } + 8 : + 9 : export type EndpointPart<Body> = string | ((input: EndpointInput<Body>) => string) + 10 : + 11 : /** + 12 : * Declarative URL construction for one route. + 13 : * + 14 : * `Endpoint` carries URL construction for one route. Routes with a canonical + 15 : * host put `baseURL` here; provider helpers can override it by configuring the + 16 : * route before selecting a model. + 17 : * + 18 : * `path` may be a string or a function of `EndpointInput`, for routes whose + 19 : * URL embeds the model id, region, or another body field (e.g. Bedrock, + 20 : * Gemini). + 21 : */ + 22 : export interface Endpoint<Body> { + 23 : readonly baseURL?: string + 24 : readonly path: EndpointPart<Body> + 25 : readonly query?: Record<string, string> + 26 : } + 27 : + 28 : export type EndpointPatch<Body> = Partial<Endpoint<Body>> + 29 : + 30 : /** Construct an `Endpoint` from a path string or path function. */ + 31 50 : export const path = <Body>(value: EndpointPart<Body>, options: Omit<Endpoint<Body>, "path"> = {}): Endpoint<Body> => ({ + 32 10 : ...options, + 33 12 : path: value, + 34 2 : }) + 35 : + 36 43 : export const merge = <Body>(base: Endpoint<Body>, patch: EndpointPatch<Body>): Endpoint<Body> => ({ + 37 10 : ...base, + 38 8 : ...patch, + 39 41 : baseURL: patch.baseURL ?? base.baseURL, + 40 32 : path: patch.path ?? base.path, + 41 44 : query: patch.query === undefined ? base.query : { ...base.query, ...patch.query }, + 42 2 : }) + 43 : + 44 34 : const renderPart = <Body>(part: EndpointPart<Body>, input: EndpointInput<Body>) => + 45 36 : typeof part === "function" ? part(input) : part + 46 : + 47 44 : export const render = <Body>(endpoint: Endpoint<Body>, input: EndpointInput<Body>) => { + 48 114 : const url = new URL(`${ProviderShared.trimBaseUrl(endpoint.baseURL ?? "")}${renderPart(endpoint.path, input)}`) + 49 69 : for (const [key, value] of Object.entries(endpoint.query ?? {})) url.searchParams.set(key, value) + 50 12 : return url + 51 : } + 52 : + 53 38 : export * as Endpoint from "./endpoint" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 25 : import { Cause, Context, Effect, Layer, Random } from "effect" + 2 96 : import { + 3 : FetchHttpClient, + 4 : Headers, + 5 : HttpClient, + 6 : HttpClientError, + 7 : HttpClientRequest, + 8 : HttpClientResponse, + 9 : } from "effect/unstable/http" + 10 292 : import { + 11 : AuthenticationReason, + 12 : ContentPolicyReason, + 13 : HttpContext, + 14 : HttpRateLimitDetails, + 15 : HttpRequestDetails, + 16 : HttpResponseDetails, + 17 : InvalidRequestReason, + 18 : LLMError, + 19 : ProviderInternalReason, + 20 : QuotaExceededReason, + 21 : RateLimitReason, + 22 : TransportReason, + 23 : UnknownProviderReason, + 24 : } from "../schema" + 25 54 : import { isContextOverflow } from "../provider-error" + 26 : + 27 : export interface Interface { + 28 : readonly execute: ( + 29 : request: HttpClientRequest.HttpClientRequest, + 30 : ) => Effect.Effect<HttpClientResponse.HttpClientResponse, LLMError> + 31 : } + 32 : + 33 82 : export class Service extends Context.Service<Service, Interface>()("@opencode/LLM/RequestExecutor") {} + 34 : + 35 25 : const BODY_LIMIT = 16_384 + 36 22 : const MAX_RETRIES = 2 + 37 26 : const BASE_DELAY_MS = 500 + 38 25 : const MAX_DELAY_MS = 10_000 + 39 30 : const REDACTED = "<redacted>" + 40 : + 41 : // One source of truth for what counts as a sensitive name across headers, + 42 : // URL query keys, and field names embedded inside request/response bodies. + 43 : // + 44 : // `SENSITIVE_NAME` is used as both a substring matcher (for free-form header + 45 : // names like `Authorization` / `X-API-Key`) and as the body-field alternation + 46 : // list. `SHORT_QUERY_NAME` covers anchored short keys like `?key=…` / `?sig=…` + 47 : // that are too generic to redact substring-style without false positives. + 48 29 : const SENSITIVE_NAME_SOURCE = + 49 127 : "authorization|api[-_]?key|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|credential|signature|x-amz-signature" + 50 62 : const SENSITIVE_NAME = new RegExp(SENSITIVE_NAME_SOURCE, "i") + 51 40 : const SHORT_QUERY_NAME = /^(key|sig)$/i + 52 81 : const SENSITIVE_BODY_FIELD = new RegExp(`(?:${SENSITIVE_NAME_SOURCE}|key)`, "i") + 53 101 : const REDACT_JSON_FIELD = new RegExp(`("(?:${SENSITIVE_BODY_FIELD.source})"\\s*:\\s*)"[^"]*"`, "gi") + 54 94 : const REDACT_QUERY_FIELD = new RegExp(`((?:${SENSITIVE_BODY_FIELD.source})=)[^&\\s"]+`, "gi") + 55 : + 56 30 : const isSensitiveHeaderName = (name: string) => SENSITIVE_NAME.test(name) + 57 : + 58 29 : const isSensitiveQueryName = (name: string) => isSensitiveHeaderName(name) || SHORT_QUERY_NAME.test(name) + 59 : + 60 0 : const redactHeaders = (headers: Headers.Headers, redactedNames: ReadonlyArray<string | RegExp>) => + 61 0 : Object.fromEntries( + 62 0 : Object.entries(Headers.redact(headers, [...redactedNames, SENSITIVE_NAME])).map(([name, value]) => [ + 63 0 : name, + 64 0 : String(value), + 65 : ]), + 66 2 : ) + 67 : + 68 0 : const redactUrl = (value: string) => { + 69 0 : if (!URL.canParse(value)) return REDACTED + 70 0 : const url = new URL(value) + 71 0 : url.searchParams.forEach((_, key) => { + 72 0 : if (isSensitiveQueryName(key)) url.searchParams.set(key, REDACTED) + 73 0 : }) + 74 2 : return url.toString() + 75 : } + 76 : + 77 0 : const normalizedHeaders = (headers: Headers.Headers) => + 78 2 : Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])) + 79 : + 80 0 : const requestId = (headers: Record<string, string>) => { + 81 0 : return ( + 82 0 : headers["x-request-id"] ?? + 83 0 : headers["request-id"] ?? + 84 0 : headers["x-amzn-requestid"] ?? + 85 0 : headers["x-amz-request-id"] ?? + 86 0 : headers["x-goog-request-id"] ?? + 87 2 : headers["cf-ray"] + 88 : ) + 89 : } + 90 : + 91 24 : const retryableStatus = (status: number) => status === 429 || status === 503 || status === 504 || status === 529 + 92 : + 93 0 : const retryAfterMs = (headers: Record<string, string>) => { + 94 0 : const millis = Number(headers["retry-after-ms"]) + 95 0 : if (Number.isFinite(millis)) return Math.max(0, millis) + 96 0 : + 97 0 : const value = headers["retry-after"] + 98 0 : if (!value) return undefined + 99 0 : + 100 0 : const seconds = Number(value) + 101 0 : if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000) + 102 0 : + 103 0 : const date = Date.parse(value) + 104 0 : if (!Number.isNaN(date)) return Math.max(0, date - Date.now()) + 105 2 : return undefined + 106 : } + 107 : + 108 0 : const addRateLimitValue = (target: Record<string, string>, key: string, value: string) => { + 109 2 : if (key.length > 0) target[key] = value + 110 : } + 111 : + 112 0 : const rateLimitDetails = (headers: Record<string, string>, retryAfter: number | undefined) => { + 113 0 : const limit: Record<string, string> = {} + 114 0 : const remaining: Record<string, string> = {} + 115 0 : const reset: Record<string, string> = {} + 116 0 : + 117 0 : Object.entries(headers).forEach(([name, value]) => { + 118 0 : const openaiLimit = /^x-ratelimit-limit-(.+)$/.exec(name)?.[1] + 119 0 : if (openaiLimit) return addRateLimitValue(limit, openaiLimit, value) + 120 0 : + 121 0 : const openaiRemaining = /^x-ratelimit-remaining-(.+)$/.exec(name)?.[1] + 122 0 : if (openaiRemaining) return addRateLimitValue(remaining, openaiRemaining, value) + 123 0 : + 124 0 : const openaiReset = /^x-ratelimit-reset-(.+)$/.exec(name)?.[1] + 125 0 : if (openaiReset) return addRateLimitValue(reset, openaiReset, value) + 126 0 : + 127 0 : const anthropic = /^anthropic-ratelimit-(.+)-(limit|remaining|reset)$/.exec(name) + 128 0 : if (!anthropic) return + 129 0 : if (anthropic[2] === "limit") return addRateLimitValue(limit, anthropic[1], value) + 130 0 : if (anthropic[2] === "remaining") return addRateLimitValue(remaining, anthropic[1], value) + 131 0 : return addRateLimitValue(reset, anthropic[1], value) + 132 0 : }) + 133 0 : + 134 0 : if ( + 135 0 : retryAfter === undefined && + 136 0 : Object.keys(limit).length === 0 && + 137 0 : Object.keys(remaining).length === 0 && + 138 0 : Object.keys(reset).length === 0 + 139 0 : ) + 140 0 : return undefined + 141 0 : + 142 0 : return new HttpRateLimitDetails({ + 143 0 : retryAfterMs: retryAfter, + 144 0 : limit: Object.keys(limit).length === 0 ? undefined : limit, + 145 0 : remaining: Object.keys(remaining).length === 0 ? undefined : remaining, + 146 0 : reset: Object.keys(reset).length === 0 ? undefined : reset, + 147 2 : }) + 148 : } + 149 : + 150 0 : const requestDetails = (request: HttpClientRequest.HttpClientRequest, redactedNames: ReadonlyArray<string | RegExp>) => + 151 0 : new HttpRequestDetails({ + 152 0 : method: request.method, + 153 0 : url: redactUrl(request.url), + 154 : headers: redactHeaders(request.headers, redactedNames), + 155 2 : }) + 156 : + 157 0 : const responseDetails = ( + 158 0 : response: HttpClientResponse.HttpClientResponse, + 159 0 : redactedNames: ReadonlyArray<string | RegExp>, + 160 0 : ) => + 161 0 : new HttpResponseDetails({ + 162 0 : status: response.status, + 163 : headers: redactHeaders(response.headers, redactedNames), + 164 2 : }) + 165 : + 166 0 : const secretValues = (request: HttpClientRequest.HttpClientRequest) => { + 167 0 : const values = new Set<string>() + 168 0 : const add = (value: string) => { + 169 0 : if (value.length < 4) return + 170 0 : values.add(value) + 171 0 : values.add(encodeURIComponent(value)) + 172 0 : } + 173 0 : + 174 0 : Object.entries(request.headers).forEach(([name, value]) => { + 175 0 : if (!isSensitiveHeaderName(name)) return + 176 0 : add(value) + 177 0 : const bearer = /^Bearer\s+(.+)$/i.exec(value)?.[1] + 178 0 : if (bearer) add(bearer) + 179 0 : }) + 180 0 : + 181 0 : if (!URL.canParse(request.url)) return values + 182 0 : new URL(request.url).searchParams.forEach((value, key) => { + 183 0 : if (isSensitiveQueryName(key)) add(value) + 184 0 : }) + 185 2 : return values + 186 : } + 187 : + 188 : // Two passes: structural (redact `"name": "value"` and `name=value` patterns + 189 : // for any field name that looks sensitive) plus literal (replace any actual + 190 : // secret values we sent in the request, in case the response echoes one back). + 191 0 : const redactBody = (body: string, request: HttpClientRequest.HttpClientRequest) => + 192 0 : Array.from(secretValues(request)).reduce( + 193 0 : (text, secret) => text.split(secret).join(REDACTED), + 194 : body.replace(REDACT_JSON_FIELD, `$1"${REDACTED}"`).replace(REDACT_QUERY_FIELD, `$1${REDACTED}`), + 195 2 : ) + 196 : + 197 0 : const responseBody = (body: string | void, request: HttpClientRequest.HttpClientRequest) => { + 198 0 : if (body === undefined) return {} + 199 0 : const redacted = redactBody(body, request) + 200 0 : if (redacted.length <= BODY_LIMIT) return { body: redacted } + 201 2 : return { body: redacted.slice(0, BODY_LIMIT), bodyTruncated: true } + 202 : } + 203 : + 204 0 : const providerMessage = (status: number, body: { readonly body?: string }) => { + 205 0 : if (body.body && body.body.length <= 500) return `Provider request failed with HTTP ${status}: ${body.body}` + 206 2 : return `Provider request failed with HTTP ${status}` + 207 : } + 208 : + 209 0 : const responseHttp = (input: { + 210 0 : readonly request: HttpClientRequest.HttpClientRequest + 211 0 : readonly response: HttpClientResponse.HttpClientResponse + 212 0 : readonly redactedNames: ReadonlyArray<string | RegExp> + 213 0 : readonly body: ReturnType<typeof responseBody> + 214 0 : readonly requestId?: string | undefined + 215 0 : readonly rateLimit?: HttpRateLimitDetails | undefined + 216 0 : }) => + 217 0 : new HttpContext({ + 218 0 : request: requestDetails(input.request, input.redactedNames), + 219 0 : response: responseDetails(input.response, input.redactedNames), + 220 0 : ...input.body, + 221 0 : requestId: input.requestId, + 222 : rateLimit: input.rateLimit, + 223 2 : }) + 224 : + 225 0 : const statusReason = (input: { + 226 0 : readonly status: number + 227 0 : readonly message: string + 228 0 : readonly retryAfterMs?: number | undefined + 229 0 : readonly rateLimit?: HttpRateLimitDetails | undefined + 230 0 : readonly http: HttpContext + 231 0 : }) => { + 232 0 : const body = input.http.body ?? "" + 233 0 : if (/content[-_\s]?policy|content_filter|safety/i.test(body)) { + 234 0 : return new ContentPolicyReason({ message: input.message, http: input.http }) + 235 0 : } + 236 0 : if (input.status === 401) { + 237 0 : return new AuthenticationReason({ message: input.message, kind: "invalid", http: input.http }) + 238 0 : } + 239 0 : if (input.status === 403) { + 240 0 : return new AuthenticationReason({ message: input.message, kind: "insufficient-permissions", http: input.http }) + 241 0 : } + 242 0 : if (input.status === 429) { + 243 0 : if (/insufficient[-_\s]?quota|quota[-_\s]?exceeded/i.test(body)) { + 244 0 : return new QuotaExceededReason({ message: input.message, http: input.http }) + 245 0 : } + 246 0 : return new RateLimitReason({ + 247 0 : message: input.message, + 248 0 : retryAfterMs: input.retryAfterMs, + 249 0 : rateLimit: input.rateLimit, + 250 0 : http: input.http, + 251 0 : }) + 252 0 : } + 253 0 : if ( + 254 0 : input.status === 400 || + 255 0 : input.status === 404 || + 256 0 : input.status === 409 || + 257 0 : input.status === 413 || + 258 0 : input.status === 422 + 259 0 : ) { + 260 0 : return new InvalidRequestReason({ + 261 0 : message: input.message, + 262 0 : classification: isContextOverflow(body) ? "context-overflow" : undefined, + 263 0 : http: input.http, + 264 0 : }) + 265 0 : } + 266 0 : if (input.status >= 500 || retryableStatus(input.status)) { + 267 0 : return new ProviderInternalReason({ + 268 0 : message: input.message, + 269 0 : status: input.status, + 270 0 : retryAfterMs: input.retryAfterMs, + 271 0 : http: input.http, + 272 0 : }) + 273 0 : } + 274 2 : return new UnknownProviderReason({ message: input.message, status: input.status, http: input.http }) + 275 : } + 276 : + 277 18 : const statusError = + 278 27 : (request: HttpClientRequest.HttpClientRequest, redactedNames: ReadonlyArray<string | RegExp>) => + 279 14 : (response: HttpClientResponse.HttpClientResponse) => + 280 15 : Effect.gen(function* () { + 281 44 : if (response.status < 400) return response + 282 0 : const body = yield* response.text.pipe(Effect.catch(() => Effect.void)) + 283 0 : const headers = normalizedHeaders(response.headers) + 284 0 : const retryAfter = retryAfterMs(headers) + 285 0 : const rateLimit = rateLimitDetails(headers, retryAfter) + 286 0 : const details = responseBody(body, request) + 287 0 : return yield* new LLMError({ + 288 0 : module: "RequestExecutor", + 289 0 : method: "execute", + 290 0 : reason: statusReason({ + 291 0 : status: response.status, + 292 0 : message: providerMessage(response.status, details), + 293 0 : retryAfterMs: retryAfter, + 294 0 : rateLimit, + 295 0 : http: responseHttp({ + 296 0 : request, + 297 0 : response, + 298 0 : redactedNames, + 299 0 : body: details, + 300 0 : requestId: requestId(headers), + 301 0 : rateLimit, + 302 0 : }), + 303 0 : }), + 304 0 : }) + 305 3 : }) + 306 : + 307 0 : const toHttpError = (redactedNames: ReadonlyArray<string | RegExp>) => (error: unknown) => { + 308 0 : const transportError = (input: { + 309 0 : readonly message: string + 310 0 : readonly kind?: string | undefined + 311 0 : readonly request?: HttpClientRequest.HttpClientRequest | undefined + 312 0 : }) => + 313 0 : new LLMError({ + 314 0 : module: "RequestExecutor", + 315 0 : method: "execute", + 316 0 : reason: new TransportReason({ + 317 0 : message: input.message, + 318 0 : kind: input.kind, + 319 0 : url: input.request ? redactUrl(input.request.url) : undefined, + 320 0 : http: input.request ? new HttpContext({ request: requestDetails(input.request, redactedNames) }) : undefined, + 321 0 : }), + 322 0 : }) + 323 0 : + 324 0 : if (Cause.isTimeoutError(error)) { + 325 0 : return transportError({ message: error.message, kind: "Timeout" }) + 326 0 : } + 327 0 : if (!HttpClientError.isHttpClientError(error)) { + 328 0 : return transportError({ message: "HTTP transport failed" }) + 329 0 : } + 330 0 : const request = "request" in error ? error.request : undefined + 331 0 : if (error.reason._tag === "TransportError") { + 332 0 : return transportError({ + 333 0 : message: error.reason.description ?? "HTTP transport failed", + 334 0 : kind: error.reason._tag, + 335 0 : request, + 336 0 : }) + 337 0 : } + 338 0 : return transportError({ + 339 0 : message: `HTTP transport failed: ${error.reason._tag}`, + 340 0 : kind: error.reason._tag, + 341 0 : request, + 342 2 : }) + 343 : } + 344 : + 345 0 : const retryDelay = (error: LLMError, attempt: number) => { + 346 0 : if (error.retryAfterMs !== undefined) return Effect.succeed(Math.min(error.retryAfterMs, MAX_DELAY_MS)) + 347 0 : return Random.nextBetween( + 348 0 : Math.min(BASE_DELAY_MS * 2 ** attempt * 0.8, MAX_DELAY_MS), + 349 0 : Math.min(BASE_DELAY_MS * 2 ** attempt * 1.2, MAX_DELAY_MS), + 350 2 : ).pipe(Effect.map((delay) => Math.round(delay))) + 351 : } + 352 : + 353 27 : const retryStatusFailures = <A, R>( + 354 8 : effect: Effect.Effect<A, LLMError, R>, + 355 23 : retries = MAX_RETRIES, + 356 16 : attempt = 0, + 357 : ): Effect.Effect<A, LLMError, R> => + 358 0 : Effect.catchTag(effect, "LLM.Error", (error): Effect.Effect<A, LLMError, R> => { + 359 0 : if (!error.retryable || retries <= 0) return Effect.fail(error) + 360 0 : return retryDelay(error, attempt).pipe( + 361 0 : Effect.flatMap((delay) => Effect.sleep(delay)), + 362 0 : Effect.flatMap(() => retryStatusFailures(effect, retries - 1, attempt + 1)), + 363 : ) + 364 2 : }) + 365 : + 366 33 : export const layer: Layer.Layer<Service, never, HttpClient.HttpClient> = Layer.effect( + 367 9 : Service, + 368 15 : Effect.gen(function* () { + 369 44 : const http = yield* HttpClient.HttpClient + 370 32 : const executeOnce = (request: HttpClientRequest.HttpClientRequest) => + 371 17 : Effect.gen(function* () { + 372 62 : const redactedNames = yield* Headers.CurrentRedactedNames + 373 19 : return yield* http + 374 17 : .execute(request) + 375 103 : .pipe(Effect.mapError(toHttpError(redactedNames)), Effect.flatMap(statusError(request, redactedNames))) + 376 3 : }) + 377 23 : return Service.of({ + 378 63 : execute: (request) => retryStatusFailures(executeOnce(request)), + 379 2 : }) + 380 1 : }), + 381 3 : ) + 382 : + 383 75 : export const fetchLayer = layer.pipe(Layer.provide(FetchHttpClient.layer)) + 384 : + 385 45 : export * as RequestExecutor from "./executor" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : import type { Stream } from "effect" + 2 54 : import * as ProviderShared from "../protocols/shared" + 3 : import type { LLMError } from "../schema" + 4 : + 5 : /** + 6 : * Decode a streaming HTTP response body into provider-protocol frames. + 7 : * + 8 : * `Framing` is the byte-stream-shaped seam between transport and protocol: + 9 : * + 10 : * - SSE (`Framing.sse`) — UTF-8 decode the body, run the SSE channel decoder, + 11 : * drop empty / `[DONE]` keep-alives. Each emitted frame is the JSON `data:` + 12 : * payload of one event. + 13 : * - AWS event stream — length-prefixed binary frames with CRC checksums. + 14 : * Each emitted frame is one parsed binary event record. + 15 : * + 16 : * The frame type is opaque to this layer; the protocol's `decode` step turns + 17 : * a frame into a typed chunk. + 18 : */ + 19 : export interface Framing<Frame> { + 20 : readonly id: string + 21 : readonly frame: (bytes: Stream.Stream<Uint8Array, LLMError>) => Stream.Stream<Frame, LLMError> + 22 : } + 23 : + 24 : /** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */ + 25 67 : export const sse: Framing<string> = { id: "sse", frame: ProviderShared.sseFraming } + 26 : + 27 36 : export * as Framing from "./framing" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| auth-options.ts | +
+ |
+ 30.0 % | +10 | +3 | +|
| auth.ts | +
+ |
+ 73.3 % | +86 | +63 | +|
| client.ts | +
+ |
+ 88.3 % | +222 | +196 | +|
| endpoint.ts | +
+ |
+ 100.0 % | +19 | +19 | +|
| executor.ts | +
+ |
+ 21.9 % | +301 | +66 | +|
| framing.ts | +
+ |
+ 100.0 % | +3 | +3 | +|
| index.ts | +
+ |
+ 100.0 % | +9 | +9 | +|
| protocol.ts | +
+ |
+ 100.0 % | +6 | +6 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| executor.ts | +
+ |
+ 21.9 % | +301 | +66 | +|
| auth-options.ts | +
+ |
+ 30.0 % | +10 | +3 | +|
| auth.ts | +
+ |
+ 73.3 % | +86 | +63 | +|
| client.ts | +
+ |
+ 88.3 % | +222 | +196 | +|
| framing.ts | +
+ |
+ 100.0 % | +3 | +3 | +|
| protocol.ts | +
+ |
+ 100.0 % | +6 | +6 | +|
| index.ts | +
+ |
+ 100.0 % | +9 | +9 | +|
| endpoint.ts | +
+ |
+ 100.0 % | +19 | +19 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| auth-options.ts | +
+ |
+ 30.0 % | +10 | +3 | +|
| auth.ts | +
+ |
+ 73.3 % | +86 | +63 | +|
| client.ts | +
+ |
+ 88.3 % | +222 | +196 | +|
| endpoint.ts | +
+ |
+ 100.0 % | +19 | +19 | +|
| executor.ts | +
+ |
+ 21.9 % | +301 | +66 | +|
| framing.ts | +
+ |
+ 100.0 % | +3 | +3 | +|
| index.ts | +
+ |
+ 100.0 % | +9 | +9 | +|
| protocol.ts | +
+ |
+ 100.0 % | +6 | +6 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 44 : export { Route, LLMClient } from "./client" + 2 : export type { + 3 : Route as RouteShape, + 4 : RouteModelInput, + 5 : RouteRoutedModelInput, + 6 : RouteDefaults, + 7 : RouteDefaultsInput, + 8 : AnyRoute, + 9 : Interface as LLMClientShape, + 10 : Service as LLMClientService, + 11 : } from "./client" + 12 27 : export * from "./executor" + 13 30 : export { Auth } from "./auth" + 14 45 : export { AuthOptions } from "./auth-options" + 15 38 : export { Endpoint } from "./endpoint" + 16 36 : export { Framing } from "./framing" + 17 38 : export { Protocol } from "./protocol" + 18 83 : export { HttpTransport, WebSocketExecutor, WebSocketTransport } from "./transport" + 19 40 : export * as Transport from "./transport" + 20 : export type { Auth as AuthShape, AuthInput, Credential, CredentialError } from "./auth" + 21 : export type { ApiKeyMode, AuthOverride, ProviderAuthOption } from "./auth-options" + 22 : export type { Endpoint as EndpointFn, EndpointInput } from "./endpoint" + 23 : export type { Framing as FramingDef } from "./framing" + 24 : export type { Protocol as ProtocolDef } from "./protocol" + 25 : export type { Transport as TransportDef, TransportRuntime } from "./transport" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Schema, type Effect } from "effect" + 2 : import type { LLMError, LLMEvent, LLMRequest, ProtocolID } from "../schema" + 3 : + 4 : /** + 5 : * The semantic API contract of one model server family. + 6 : * + 7 : * A `Protocol` owns the parts of a route that are intrinsic to "what does + 8 : * this API look like": how a common `LLMRequest` becomes a provider-native + 9 : * body, what schema that body must satisfy before it is JSON-encoded, and + 10 : * how the streaming response decodes back into common `LLMEvent`s. + 11 : * + 12 : * Examples: + 13 : * + 14 : * - `OpenAIChat.protocol` — chat completions style + 15 : * - `OpenAIResponses.protocol` — responses API + 16 : * - `AnthropicMessages.protocol` — messages API with content blocks + 17 : * - `Gemini.protocol` — generateContent + 18 : * - `BedrockConverse.protocol` — Converse with binary event-stream framing + 19 : * + 20 : * A `Protocol` is **not** a deployment. It does not know which URL, which + 21 : * headers, or which auth scheme to use. Those are deployment concerns owned + 22 : * by `Route.make(...)` along with the chosen `Endpoint`, `Auth`, + 23 : * and `Framing`. This separation is what lets DeepSeek, TogetherAI, Cerebras, + 24 : * etc. all reuse `OpenAIChat.protocol` without forking 300 lines per provider. + 25 : * + 26 : * The four type parameters reflect the pipeline: + 27 : * + 28 : * - `Body` — provider-native request body candidate. `Route.make(...)` + 29 : * validates and JSON-encodes it with `body.schema`. + 30 : * - `Frame` — one unit of the framed response stream. SSE: a JSON data + 31 : * string. AWS event stream: a parsed binary frame. + 32 : * - `Event` — schema-decoded provider event produced from one frame. + 33 : * - `State` — accumulator threaded through `stream.step` to translate event + 34 : * sequences into `LLMEvent` sequences. + 35 : */ + 36 : export interface Protocol<Body, Frame, Event, State> { + 37 : /** Stable id for the wire protocol implementation. */ + 38 : readonly id: ProtocolID + 39 : /** Request side: schema for the provider-native body and how to build it. */ + 40 : readonly body: ProtocolBody<Body> + 41 : /** Response side: streaming state machine. */ + 42 : readonly stream: ProtocolStream<Frame, Event, State> + 43 : } + 44 : + 45 : export interface ProtocolBody<Body> { + 46 : /** Schema for the validated provider-native body sent as the JSON request. */ + 47 : readonly schema: Schema.Codec<Body, unknown> + 48 : /** Build the provider-native body from a common `LLMRequest`. */ + 49 : readonly from: (request: LLMRequest) => Effect.Effect<Body, LLMError> + 50 : } + 51 : + 52 : export interface ProtocolStream<Frame, Event, State> { + 53 : /** Schema for one decoded streaming event, decoded from a transport frame. */ + 54 : readonly event: Schema.Codec<Event, Frame> + 55 : /** Initial parser state. Called once per response with the resolved request. */ + 56 : readonly initial: (request: LLMRequest) => State + 57 : /** Translate one event into emitted `LLMEvent`s plus the next state. */ + 58 : readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], LLMError> + 59 : /** Optional request-completion signal for transports that do not end naturally. */ + 60 : readonly terminal?: (event: Event) => boolean + 61 : /** Optional flush emitted when the framed stream ends. */ + 62 : readonly onHalt?: (state: State) => ReadonlyArray<LLMEvent> + 63 : } + 64 : + 65 : /** + 66 : * Construct a `Protocol` from its body and stream pieces: + 67 : * + 68 : * - `body.schema` infers the provider-native request body shape. + 69 : * - `body.from` ties the common `LLMRequest` to the provider body. + 70 : * - `stream.event` infers the decoded streaming event and the wire frame. + 71 : * - `stream.initial`, `stream.step`, and `stream.onHalt` infer the parser state. + 72 : * + 73 : * Provider implementations should usually call `Protocol.make({ ... })` + 74 : * without explicit type arguments; the schemas and parser functions are the + 75 : * source of truth. The constructor remains as the public seam for future + 76 : * cross-cutting concerns such as tracing or instrumentation. + 77 : */ + 78 19 : export const make = <Body, Frame, Event, State>( + 79 10 : input: Protocol<Body, Frame, Event, State>, + 80 6 : ): Protocol<Body, Frame, Event, State> => input + 81 : + 82 65 : export const jsonEvent = <const S extends Schema.Top>(schema: S) => Schema.fromJsonString(schema) + 83 : + 84 38 : export * as Protocol from "./protocol" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 40 : import { Effect, Stream } from "effect" + 2 47 : import { Headers, HttpClientRequest } from "effect/unstable/http" + 3 31 : import { Auth } from "../auth" + 4 55 : import { render as renderEndpoint } from "../endpoint" + 5 37 : import { Framing, type Framing as FramingDef } from "../framing" + 6 : import type { Transport, TransportPrepareInput } from "./index" + 7 57 : import * as ProviderShared from "../../protocols/shared" + 8 48 : import { mergeJsonRecords, type LLMRequest } from "../../schema" + 9 : + 10 : export type JsonRequestInput<Body> = TransportPrepareInput<Body> + 11 : + 12 : export interface JsonRequestParts<Body = unknown> { + 13 : readonly url: string + 14 : readonly jsonBody: Body | Record<string, unknown> + 15 : readonly bodyText: string + 16 : readonly headers: Headers.Headers + 17 : } + 18 : + 19 : export interface HttpPrepared<Frame> { + 20 : readonly request: HttpClientRequest.HttpClientRequest + 21 : readonly framing: FramingDef<Frame> + 22 : } + 23 : + 24 36 : const applyQuery = (url: string, query: Record<string, string> | undefined) => { + 25 24 : if (!query) return url + 26 0 : const next = new URL(url) + 27 0 : Object.entries(query).forEach(([key, value]) => next.searchParams.set(key, value)) + 28 2 : return next.toString() + 29 : } + 30 : + 31 49 : const PROTOCOL_BODY_OVERLAY_DENYLIST = new Set([ + 32 12 : "content", + 33 13 : "contents", + 34 21 : "frequencyPenalty", + 35 22 : "frequency_penalty", + 36 21 : "generationConfig", + 37 20 : "inferenceConfig", + 38 10 : "input", + 39 14 : "maxTokens", + 40 15 : "max_tokens", + 41 13 : "messages", + 42 10 : "model", + 43 20 : "presencePenalty", + 44 21 : "presence_penalty", + 45 19 : "responseFormat", + 46 20 : "response_format", + 47 9 : "seed", + 48 9 : "stop", + 49 18 : "stopSequences", + 50 19 : "stop_sequences", + 51 11 : "stream", + 52 18 : "streamOptions", + 53 19 : "stream_options", + 54 11 : "system", + 55 22 : "systemInstruction", + 56 23 : "system_instruction", + 57 16 : "temperature", + 58 13 : "thinking", + 59 15 : "toolChoice", + 60 15 : "toolConfig", + 61 16 : "tool_choice", + 62 16 : "tool_config", + 63 10 : "tools", + 64 9 : "topK", + 65 9 : "topP", + 66 10 : "top_k", + 67 8 : "top_p", + 68 3 : ]) + 69 : + 70 41 : const forbiddenBodyOverlayKeys = (body: Record<string, unknown>) => + 71 73 : Object.keys(body).filter((key) => PROTOCOL_BODY_OVERLAY_DENYLIST.has(key)) + 72 : + 73 53 : const bodyWithOverlay = <Body>(body: Body, request: LLMRequest, encodeBody: (body: Body) => string) => + 74 15 : Effect.gen(function* () { + 75 95 : if (request.http?.body === undefined) return { jsonBody: body, bodyText: encodeBody(body) } + 76 68 : const forbiddenKeys = forbiddenBodyOverlayKeys(request.http.body) + 77 32 : if (forbiddenKeys.length > 0) + 78 0 : return yield* ProviderShared.invalidRequest( + 79 0 : `http.body cannot overlay protocol-owned field(s): ${forbiddenKeys.join(", ")}`, + 80 2 : ) + 81 39 : if (ProviderShared.isRecord(body)) { + 82 69 : const overlaid = mergeJsonRecords(body, request.http.body) ?? {} + 83 76 : return { jsonBody: overlaid, bodyText: ProviderShared.encodeJson(overlaid) } + 84 0 : } + 85 0 : return yield* ProviderShared.invalidRequest("http.body can only overlay JSON object request bodies") + 86 2 : }) + 87 : + 88 41 : export const jsonRequestParts = <Body>(input: JsonRequestInput<Body>) => + 89 15 : Effect.gen(function* () { + 90 23 : const url = applyQuery( + 91 89 : renderEndpoint(input.endpoint, { request: input.request, body: input.body }).toString(), + 92 25 : input.request.http?.query, + 93 4 : ) + 94 83 : const body = yield* bodyWithOverlay(input.body, input.request, input.encodeBody) + 95 54 : const headers = yield* Auth.toEffect(input.auth)({ + 96 27 : request: input.request, + 97 19 : method: "POST", + 98 8 : url, + 99 24 : body: body.bodyText, + 100 37 : headers: Headers.fromInput({ + 101 53 : ...input.headers?.({ request: input.request }), + 102 31 : ...input.request.http?.headers, + 103 4 : }), + 104 5 : }) + 105 73 : return { url, jsonBody: body.jsonBody, bodyText: body.bodyText, headers } + 106 2 : }) + 107 : + 108 : export interface HttpJsonInput<_Body, Frame> { + 109 : readonly framing: FramingDef<Frame> + 110 : } + 111 : + 112 : export type HttpJsonPatch<Body, Frame> = Partial<HttpJsonInput<Body, Frame>> + 113 : + 114 : export interface HttpJsonTransport<Body, Frame> extends Transport<Body, HttpPrepared<Frame>, Frame> { + 115 : readonly with: (patch: HttpJsonPatch<Body, Frame>) => HttpJsonTransport<Body, Frame> + 116 : } + 117 : + 118 37 : export const httpJson = <Body, Frame>(input: HttpJsonInput<Body, Frame>): HttpJsonTransport<Body, Frame> => ({ + 119 18 : id: "http-json", + 120 8 : with: (patch) => httpJson({ ...input, ...patch }), + 121 26 : prepare: (prepareInput) => + 122 25 : jsonRequestParts({ + 123 14 : ...prepareInput, + 124 8 : }).pipe( + 125 27 : Effect.map((parts) => ({ + 126 103 : request: ProviderShared.jsonPost({ url: parts.url, body: parts.bodyText, headers: parts.headers }), + 127 24 : framing: input.framing, + 128 2 : })), + 129 3 : ), + 130 39 : frames: (prepared, request, runtime) => + 131 14 : Stream.unwrap( + 132 13 : runtime.http + 133 26 : .execute(prepared.request) + 134 5 : .pipe( + 135 24 : Effect.map((response) => + 136 23 : prepared.framing.frame( + 137 21 : response.stream.pipe( + 138 0 : Stream.mapError((error) => + 139 0 : ProviderShared.eventError( + 140 0 : `${request.model.provider}/${request.model.route.id}`, + 141 0 : `Failed to read ${request.model.provider}/${request.model.route.id} stream`, + 142 : ProviderShared.errorText(error), + 143 : ), + 144 1 : ), + 145 1 : ), + 146 : ), + 147 1 : ), + 148 1 : ), + 149 1 : ), + 150 2 : }) + 151 : + 152 25 : export const sseJson = { + 153 22 : id: "http-json/sse", + 154 45 : with: <Body>() => httpJson<Body, string>({ framing: Framing.sse }), + 155 1 : } as const ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| http.ts | +
+ |
+ 91.6 % | +119 | +109 | +|
| index.ts | +
+ |
+ 100.0 % | +2 | +2 | +|
| websocket.ts | +
+ |
+ 15.5 % | +219 | +34 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| websocket.ts | +
+ |
+ 15.5 % | +219 | +34 | +|
| http.ts | +
+ |
+ 91.6 % | +119 | +109 | +|
| index.ts | +
+ |
+ 100.0 % | +2 | +2 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| http.ts | +
+ |
+ 91.6 % | +119 | +109 | +|
| index.ts | +
+ |
+ 100.0 % | +2 | +2 | +|
| websocket.ts | +
+ |
+ 15.5 % | +219 | +34 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : import type { Effect, Stream } from "effect" + 2 : import type { Endpoint } from "../endpoint" + 3 : import type { Auth } from "../auth" + 4 : import type { Interface as RequestExecutorInterface } from "../executor" + 5 : import type { Interface as WebSocketExecutorInterface } from "./websocket" + 6 : import type { LLMError, LLMRequest } from "../../schema" + 7 : + 8 : export interface TransportRuntime { + 9 : readonly http: RequestExecutorInterface + 10 : readonly webSocket?: WebSocketExecutorInterface + 11 : } + 12 : + 13 : export interface Transport<Body, Prepared, Frame> { + 14 : readonly id: string + 15 : readonly prepare: (input: TransportPrepareInput<Body>) => Effect.Effect<Prepared, LLMError> + 16 : readonly frames: ( + 17 : prepared: Prepared, + 18 : request: LLMRequest, + 19 : runtime: TransportRuntime, + 20 : ) => Stream.Stream<Frame, LLMError> + 21 : } + 22 : + 23 : export interface TransportPrepareInput<Body> { + 24 : readonly body: Body + 25 : readonly request: LLMRequest + 26 : readonly endpoint: Endpoint<Body> + 27 : readonly auth: Auth + 28 : readonly encodeBody: (body: Body) => string + 29 : readonly headers?: (input: { readonly request: LLMRequest }) => Record<string, string> + 30 : } + 31 : + 32 40 : export * as HttpTransport from "./http" + 33 67 : export { WebSocketExecutor, WebSocketTransport } from "./websocket" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Cause, Context, Effect, Layer, Queue, Stream } from "effect" + 2 : import { Headers } from "effect/unstable/http" + 3 57 : import { LLMError, TransportReason } from "../../schema" + 4 40 : import * as HttpTransport from "./http" + 5 : import type { Transport } from "./index" + 6 : + 7 : export interface WebSocketRequest { + 8 : readonly url: string + 9 : readonly headers: Headers.Headers + 10 : } + 11 : + 12 : export interface WebSocketConnection { + 13 : readonly sendText: (message: string) => Effect.Effect<void, LLMError> + 14 : readonly messages: Stream.Stream<string | Uint8Array, LLMError> + 15 : readonly close: Effect.Effect<void, never> + 16 : } + 17 : + 18 : export interface Interface { + 19 : readonly open: (input: WebSocketRequest) => Effect.Effect<WebSocketConnection, LLMError> + 20 : } + 21 : + 22 : type WebSocketConstructorWithHeaders = new ( + 23 : url: string, + 24 : options?: { readonly headers?: Headers.Headers }, + 25 : ) => globalThis.WebSocket + 26 : + 27 84 : export class Service extends Context.Service<Service, Interface>()("@opencode/LLM/WebSocketExecutor") {} + 28 : + 29 0 : const transportError = ( + 30 0 : method: string, + 31 0 : message: string, + 32 0 : input: { readonly url?: string; readonly kind?: string } = {}, + 33 0 : ) => + 34 0 : new LLMError({ + 35 0 : module: "WebSocketExecutor", + 36 0 : method, + 37 : reason: new TransportReason({ message, url: input.url, kind: input.kind }), + 38 2 : }) + 39 : + 40 0 : const eventMessage = (event: Event) => { + 41 0 : if ("message" in event && typeof event.message === "string") return event.message + 42 2 : return event.type + 43 : } + 44 : + 45 0 : const binaryMessage = (data: unknown) => { + 46 0 : if (data instanceof Uint8Array) return data + 47 0 : if (data instanceof ArrayBuffer) return new Uint8Array(data) + 48 0 : if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + 49 2 : return undefined + 50 : } + 51 : + 52 0 : const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => { + 53 0 : if (ws.readyState === globalThis.WebSocket.OPEN) return Effect.void + 54 0 : if (ws.readyState === globalThis.WebSocket.CLOSING || ws.readyState === globalThis.WebSocket.CLOSED) { + 55 0 : return Effect.fail( + 56 0 : transportError("open", `WebSocket closed before opening (state ${ws.readyState})`, { + 57 0 : url: input.url, + 58 0 : kind: "open", + 59 0 : }), + 60 0 : ) + 61 0 : } + 62 0 : return Effect.callback<void, LLMError>((resume, signal) => { + 63 0 : const cleanup = () => { + 64 0 : ws.removeEventListener("open", onOpen) + 65 0 : ws.removeEventListener("error", onError) + 66 0 : ws.removeEventListener("close", onClose) + 67 0 : signal.removeEventListener("abort", onAbort) + 68 0 : } + 69 0 : const onAbort = () => { + 70 0 : cleanup() + 71 0 : if (ws.readyState !== globalThis.WebSocket.CLOSED && ws.readyState !== globalThis.WebSocket.CLOSING) + 72 0 : ws.close(1000) + 73 0 : } + 74 0 : const onOpen = () => { + 75 0 : cleanup() + 76 0 : resume(Effect.void) + 77 0 : } + 78 0 : const onError = (event: Event) => { + 79 0 : cleanup() + 80 0 : resume( + 81 0 : Effect.fail( + 82 0 : transportError("open", `Failed to open WebSocket: ${eventMessage(event)}`, { url: input.url, kind: "open" }), + 83 0 : ), + 84 0 : ) + 85 0 : } + 86 0 : const onClose = (event: CloseEvent) => { + 87 0 : cleanup() + 88 0 : resume( + 89 0 : Effect.fail( + 90 0 : transportError("open", `WebSocket closed before opening with code ${event.code}`, { + 91 0 : url: input.url, + 92 0 : kind: "open", + 93 0 : }), + 94 0 : ), + 95 0 : ) + 96 0 : } + 97 0 : ws.addEventListener("open", onOpen, { once: true }) + 98 0 : ws.addEventListener("error", onError, { once: true }) + 99 0 : ws.addEventListener("close", onClose, { once: true }) + 100 0 : signal.addEventListener("abort", onAbort, { once: true }) + 101 2 : }) + 102 : } + 103 : + 104 0 : const webSocketUrl = (value: string) => + 105 0 : Effect.try({ + 106 0 : try: () => { + 107 0 : const url = new URL(value) + 108 0 : if (url.protocol === "https:") { + 109 0 : url.protocol = "wss:" + 110 0 : return url.toString() + 111 0 : } + 112 0 : if (url.protocol === "http:") { + 113 0 : url.protocol = "ws:" + 114 0 : return url.toString() + 115 0 : } + 116 0 : throw new Error(`Unsupported WebSocket URL protocol ${url.protocol}`) + 117 0 : }, + 118 0 : catch: (error) => + 119 0 : transportError("prepare", error instanceof Error ? error.message : "Invalid WebSocket URL", { + 120 0 : url: value, + 121 0 : kind: "websocket", + 122 : }), + 123 2 : }) + 124 : + 125 0 : export const open = (input: WebSocketRequest) => + 126 0 : Effect.try({ + 127 0 : try: () => + 128 0 : new (globalThis.WebSocket as unknown as WebSocketConstructorWithHeaders)(input.url, { headers: input.headers }), + 129 0 : catch: (error) => + 130 0 : transportError("open", error instanceof Error ? error.message : "Failed to construct WebSocket", { + 131 0 : url: input.url, + 132 0 : kind: "open", + 133 0 : }), + 134 2 : }).pipe(Effect.flatMap((ws) => fromWebSocket(ws, input))) + 135 : + 136 66 : export const layer: Layer.Layer<Service> = Layer.succeed(Service, Service.of({ open })) + 137 : + 138 0 : export const fromWebSocket = ( + 139 0 : ws: globalThis.WebSocket, + 140 0 : input: WebSocketRequest, + 141 0 : ): Effect.Effect<WebSocketConnection, LLMError> => + 142 0 : Effect.gen(function* () { + 143 0 : yield* waitOpen(ws, input) + 144 0 : const messages = yield* Queue.bounded<string | Uint8Array, LLMError | Cause.Done<void>>(128) + 145 0 : + 146 0 : const onMessage = (event: MessageEvent) => { + 147 0 : if (typeof event.data === "string") return Queue.offerUnsafe(messages, event.data) + 148 0 : const binary = binaryMessage(event.data) + 149 0 : if (binary) return Queue.offerUnsafe(messages, binary) + 150 0 : Queue.failCauseUnsafe( + 151 0 : messages, + 152 0 : Cause.fail( + 153 0 : transportError("message", "Unsupported WebSocket message payload", { url: input.url, kind: "message" }), + 154 0 : ), + 155 0 : ) + 156 0 : } + 157 0 : const onError = (event: Event) => { + 158 0 : Queue.failCauseUnsafe( + 159 0 : messages, + 160 0 : Cause.fail( + 161 0 : transportError("message", `WebSocket error: ${eventMessage(event)}`, { url: input.url, kind: "message" }), + 162 0 : ), + 163 0 : ) + 164 0 : } + 165 0 : const onClose = (event: CloseEvent) => { + 166 0 : if (event.code === 1000 || event.code === 1005) return Queue.endUnsafe(messages) + 167 0 : Queue.failCauseUnsafe( + 168 0 : messages, + 169 0 : Cause.fail( + 170 0 : transportError("message", `WebSocket closed with code ${event.code}`, { url: input.url, kind: "close" }), + 171 0 : ), + 172 0 : ) + 173 0 : } + 174 0 : const cleanup = Effect.sync(() => { + 175 0 : ws.removeEventListener("message", onMessage) + 176 0 : ws.removeEventListener("error", onError) + 177 0 : ws.removeEventListener("close", onClose) + 178 0 : }).pipe(Effect.andThen(Queue.shutdown(messages))) + 179 0 : + 180 0 : ws.addEventListener("message", onMessage) + 181 0 : ws.addEventListener("error", onError) + 182 0 : ws.addEventListener("close", onClose) + 183 0 : + 184 0 : return { + 185 0 : sendText: (message) => + 186 0 : Effect.try({ + 187 0 : try: () => ws.send(message), + 188 0 : catch: (error) => + 189 0 : transportError("sendText", error instanceof Error ? error.message : "Failed to send WebSocket message", { + 190 0 : url: input.url, + 191 0 : kind: "write", + 192 0 : }), + 193 0 : }), + 194 0 : messages: Stream.fromQueue(messages), + 195 0 : close: cleanup.pipe( + 196 0 : Effect.andThen( + 197 0 : Effect.sync(() => { + 198 0 : if (ws.readyState === globalThis.WebSocket.CLOSED || ws.readyState === globalThis.WebSocket.CLOSING) return + 199 0 : ws.close(1000) + 200 0 : }), + 201 0 : ), + 202 0 : ), + 203 : } + 204 2 : }) + 205 : + 206 0 : export const messageText = (message: string | Uint8Array, decoder: TextDecoder) => + 207 2 : typeof message === "string" ? message : decoder.decode(message) + 208 : + 209 : export interface JsonPrepared { + 210 : readonly url: string + 211 : readonly headers: Headers.Headers + 212 : readonly message: string + 213 : } + 214 : + 215 : export interface JsonInput<Body, Message> { + 216 : readonly toMessage: (body: Body | Record<string, unknown>) => Effect.Effect<Message, LLMError> + 217 : readonly encodeMessage: (message: Message) => string + 218 : } + 219 : + 220 : export type JsonPatch<Body, Message> = Partial<JsonInput<Body, Message>> + 221 : + 222 : export interface JsonTransport<Body, Message> extends Transport<Body, JsonPrepared, string> { + 223 : readonly with: (patch: JsonPatch<Body, Message>) => JsonTransport<Body, Message> + 224 : } + 225 : + 226 33 : export const json = <Body, Message>(input: JsonInput<Body, Message>): JsonTransport<Body, Message> => ({ + 227 23 : id: "websocket-json", + 228 8 : with: (patch) => json({ ...input, ...patch }), + 229 0 : prepare: (prepareInput) => + 230 0 : Effect.gen(function* () { + 231 0 : const parts = yield* HttpTransport.jsonRequestParts({ + 232 0 : ...prepareInput, + 233 0 : }) + 234 0 : return { + 235 0 : url: yield* webSocketUrl(parts.url), + 236 0 : headers: parts.headers, + 237 0 : message: input.encodeMessage(yield* input.toMessage(parts.jsonBody)), + 238 : } + 239 3 : }), + 240 0 : frames: (prepared, _request, runtime) => { + 241 0 : const webSocket = runtime.webSocket + 242 0 : if (!webSocket) { + 243 0 : return Stream.fail( + 244 0 : transportError("json", "WebSocket JSON transport requires WebSocketExecutor.Service", { + 245 0 : url: prepared.url, + 246 0 : kind: "websocket", + 247 0 : }), + 248 0 : ) + 249 0 : } + 250 0 : const decoder = new TextDecoder() + 251 0 : return Stream.unwrap( + 252 0 : Effect.gen(function* () { + 253 0 : const connection = yield* Effect.acquireRelease( + 254 0 : webSocket.open({ url: prepared.url, headers: prepared.headers }), + 255 0 : (connection) => connection.close, + 256 0 : ) + 257 0 : yield* connection.sendText(prepared.message) + 258 0 : return connection.messages.pipe(Stream.map((message) => messageText(message, decoder))) + 259 0 : }), + 260 1 : ) + 261 : }, + 262 2 : }) + 263 : + 264 31 : export const jsonTransport = { + 265 23 : id: "websocket-json", + 266 11 : with: json, + 267 2 : } as const + 268 : + 269 35 : export const WebSocketExecutor = { + 270 10 : Service, + 271 8 : layer, + 272 7 : open, + 273 16 : fromWebSocket, + 274 12 : messageText, + 275 2 : } as const + 276 : + 277 36 : export const WebSocketTransport = { + 278 7 : json, + 279 14 : jsonTransport, + 280 1 : } as const ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 0 : import { Schema } from "effect" + 2 137 : import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids" + 3 : + 4 160 : export const ProviderFailureClassification = Schema.Literal("context-overflow") + 5 : export type ProviderFailureClassification = typeof ProviderFailureClassification.Type + 6 : + 7 162 : export class HttpRequestDetails extends Schema.Class<HttpRequestDetails>("LLM.HttpRequestDetails")({ + 8 48 : method: Schema.String, + 9 42 : url: Schema.String, + 10 106 : headers: Schema.Record(Schema.String, Schema.String), + 11 10 : }) {} + 12 : + 13 166 : export class HttpResponseDetails extends Schema.Class<HttpResponseDetails>("LLM.HttpResponseDetails")({ + 14 48 : status: Schema.Number, + 15 106 : headers: Schema.Record(Schema.String, Schema.String), + 16 10 : }) {} + 17 : + 18 170 : export class HttpRateLimitDetails extends Schema.Class<HttpRateLimitDetails>("LLM.HttpRateLimitDetails")({ + 19 94 : retryAfterMs: Schema.optional(Schema.Number), + 20 140 : limit: Schema.optional(Schema.Record(Schema.String, Schema.String)), + 21 148 : remaining: Schema.optional(Schema.Record(Schema.String, Schema.String)), + 22 136 : reset: Schema.optional(Schema.Record(Schema.String, Schema.String)), + 23 10 : }) {} + 24 : + 25 134 : export class HttpContext extends Schema.Class<HttpContext>("LLM.HttpContext")({ + 26 60 : request: HttpRequestDetails, + 27 98 : response: Schema.optional(HttpResponseDetails), + 28 78 : body: Schema.optional(Schema.String), + 29 98 : bodyTruncated: Schema.optional(Schema.Boolean), + 30 88 : requestId: Schema.optional(Schema.String), + 31 98 : rateLimit: Schema.optional(HttpRateLimitDetails), + 32 10 : }) {} + 33 : + 34 170 : export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("LLM.Error.InvalidRequest")({ + 35 74 : _tag: Schema.tag("InvalidRequest"), + 36 50 : message: Schema.String, + 37 88 : parameter: Schema.optional(Schema.String), + 38 130 : classification: Schema.optional(ProviderFailureClassification), + 39 108 : providerMetadata: Schema.optional(ProviderMetadata), + 40 70 : http: Schema.optional(HttpContext), + 41 0 : }) { + 42 0 : get retryable() { + 43 18 : return false + 44 : } + 45 2 : } + 46 : + 47 142 : export class NoRouteReason extends Schema.Class<NoRouteReason>("LLM.Error.NoRoute")({ + 48 60 : _tag: Schema.tag("NoRoute"), + 49 34 : route: RouteID, + 50 46 : provider: ProviderID, + 51 30 : model: ModelID, + 52 0 : }) { + 53 0 : get retryable() { + 54 0 : return false + 55 0 : } + 56 0 : + 57 0 : get message() { + 58 82 : return `No LLM route for ${this.provider}/${this.model} using ${this.route}` + 59 : } + 60 2 : } + 61 : + 62 170 : export class AuthenticationReason extends Schema.Class<AuthenticationReason>("LLM.Error.Authentication")({ + 63 74 : _tag: Schema.tag("Authentication"), + 64 50 : message: Schema.String, + 65 196 : kind: Schema.Literals(["missing", "invalid", "expired", "insufficient-permissions", "unknown"]), + 66 108 : providerMetadata: Schema.optional(ProviderMetadata), + 67 70 : http: Schema.optional(HttpContext), + 68 0 : }) { + 69 0 : get retryable() { + 70 18 : return false + 71 : } + 72 2 : } + 73 : + 74 150 : export class RateLimitReason extends Schema.Class<RateLimitReason>("LLM.Error.RateLimit")({ + 75 64 : _tag: Schema.tag("RateLimit"), + 76 50 : message: Schema.String, + 77 94 : retryAfterMs: Schema.optional(Schema.Number), + 78 102 : rateLimit: Schema.optional(HttpRateLimitDetails), + 79 108 : providerMetadata: Schema.optional(ProviderMetadata), + 80 70 : http: Schema.optional(HttpContext), + 81 0 : }) { + 82 0 : get retryable() { + 83 17 : return true + 84 : } + 85 2 : } + 86 : + 87 166 : export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("LLM.Error.QuotaExceeded")({ + 88 72 : _tag: Schema.tag("QuotaExceeded"), + 89 50 : message: Schema.String, + 90 108 : providerMetadata: Schema.optional(ProviderMetadata), + 91 70 : http: Schema.optional(HttpContext), + 92 0 : }) { + 93 0 : get retryable() { + 94 18 : return false + 95 : } + 96 2 : } + 97 : + 98 166 : export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("LLM.Error.ContentPolicy")({ + 99 72 : _tag: Schema.tag("ContentPolicy"), + 100 50 : message: Schema.String, + 101 108 : providerMetadata: Schema.optional(ProviderMetadata), + 102 70 : http: Schema.optional(HttpContext), + 103 0 : }) { + 104 0 : get retryable() { + 105 18 : return false + 106 : } + 107 2 : } + 108 : + 109 178 : export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("LLM.Error.ProviderInternal")({ + 110 78 : _tag: Schema.tag("ProviderInternal"), + 111 50 : message: Schema.String, + 112 48 : status: Schema.Number, + 113 94 : retryAfterMs: Schema.optional(Schema.Number), + 114 108 : providerMetadata: Schema.optional(ProviderMetadata), + 115 70 : http: Schema.optional(HttpContext), + 116 0 : }) { + 117 0 : get retryable() { + 118 17 : return true + 119 : } + 120 2 : } + 121 : + 122 150 : export class TransportReason extends Schema.Class<TransportReason>("LLM.Error.Transport")({ + 123 64 : _tag: Schema.tag("Transport"), + 124 50 : message: Schema.String, + 125 78 : kind: Schema.optional(Schema.String), + 126 76 : url: Schema.optional(Schema.String), + 127 70 : http: Schema.optional(HttpContext), + 128 0 : }) { + 129 0 : get retryable() { + 130 18 : return false + 131 : } + 132 2 : } + 133 : + 134 122 : export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOutputReason>( + 135 66 : "LLM.Error.InvalidProviderOutput", + 136 10 : )({ + 137 88 : _tag: Schema.tag("InvalidProviderOutput"), + 138 50 : message: Schema.String, + 139 80 : route: Schema.optional(Schema.String), + 140 76 : raw: Schema.optional(Schema.String), + 141 104 : providerMetadata: Schema.optional(ProviderMetadata), + 142 0 : }) { + 143 0 : get retryable() { + 144 18 : return false + 145 : } + 146 2 : } + 147 : + 148 174 : export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("LLM.Error.UnknownProvider")({ + 149 76 : _tag: Schema.tag("UnknownProvider"), + 150 50 : message: Schema.String, + 151 82 : status: Schema.optional(Schema.Number), + 152 108 : providerMetadata: Schema.optional(ProviderMetadata), + 153 70 : http: Schema.optional(HttpContext), + 154 0 : }) { + 155 0 : get retryable() { + 156 18 : return false + 157 : } + 158 2 : } + 159 : + 160 90 : export const LLMErrorReason = Schema.Union([ + 161 46 : InvalidRequestReason, + 162 32 : NoRouteReason, + 163 46 : AuthenticationReason, + 164 36 : RateLimitReason, + 165 44 : QuotaExceededReason, + 166 44 : ContentPolicyReason, + 167 50 : ProviderInternalReason, + 168 36 : TransportReason, + 169 60 : InvalidProviderOutputReason, + 170 44 : UnknownProviderReason, + 171 76 : ]).pipe(Schema.toTaggedUnion("_tag")) + 172 : export type LLMErrorReason = Schema.Schema.Type<typeof LLMErrorReason> + 173 : + 174 142 : export class LLMError extends Schema.TaggedErrorClass<LLMError>()("LLM.Error", { + 175 48 : module: Schema.String, + 176 48 : method: Schema.String, + 177 46 : reason: LLMErrorReason, + 178 10 : }) { + 179 0 : override readonly cause = this.reason + 180 0 : + 181 0 : get retryable() { + 182 0 : return this.reason.retryable + 183 0 : } + 184 0 : + 185 0 : get retryAfterMs() { + 186 0 : return "retryAfterMs" in this.reason ? this.reason.retryAfterMs : undefined + 187 0 : } + 188 0 : + 189 0 : override get message() { + 190 68 : return `${this.module}.${this.method}: ${this.reason.message}` + 191 : } + 192 2 : } + 193 : + 194 : /** + 195 : * Failure type for tool execute handlers. Handlers must map their internal + 196 : * errors to this shape; the runtime catches `ToolFailure`s and surfaces them + 197 : * as `tool-error` events plus a `tool-result` of `type: "error"` so the model + 198 : * can self-correct. + 199 : * + 200 : * Anything thrown or yielded by a handler that is not a `ToolFailure` is + 201 : * treated as a defect and fails the stream. + 202 : */ + 203 160 : export class ToolFailure extends Schema.TaggedErrorClass<ToolFailure>()("LLM.ToolFailure", { + 204 50 : message: Schema.String, + 205 84 : error: Schema.optional(Schema.Defect()), + 206 144 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + 207 9 : }) {} ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 0 : import { Schema } from "effect" + 2 99 : import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids" + 3 40 : import { ModelSchema } from "./options" + 4 96 : import { Message, ToolCallPart, ToolOutput, ToolResultPart, ToolResultValue, type ContentPart } from "./messages" + 5 57 : import { ProviderFailureClassification } from "./errors" + 6 : + 7 : /** + 8 : * Token usage reported by an LLM provider. + 9 : * + 10 : * **Inclusive totals** (match AI SDK / OpenAI / LangChain convention — a + 11 : * reader from any of those ecosystems sees the number they expect): + 12 : * + 13 : * - `inputTokens` — total prompt tokens, *including* cached reads/writes. + 14 : * - `outputTokens` — total output tokens, *including* reasoning. + 15 : * - `totalTokens` — provider-supplied total, or `inputTokens + outputTokens`. + 16 : * + 17 : * **Non-overlapping breakdown** (every field is independently meaningful; + 18 : * consumers never have to subtract): + 19 : * + 20 : * - `nonCachedInputTokens` — the "fresh" portion of the prompt. + 21 : * - `cacheReadInputTokens` — input tokens served from cache. + 22 : * - `cacheWriteInputTokens` — input tokens written to cache. + 23 : * - `reasoningTokens` — subset of `outputTokens` spent on hidden reasoning. + 24 : * + 25 : * **Invariant**: `nonCachedInputTokens + cacheReadInputTokens + + 26 : * cacheWriteInputTokens = inputTokens`, and `reasoningTokens ≤ outputTokens`. + 27 : * Each protocol mapper computes whichever side it doesn't get natively, + 28 : * with `Math.max(0, …)` clamping for defense against provider bugs. Because + 29 : * every breakdown field is stored independently, downstream consumers can + 30 : * read whatever they need (cost-by-category, context-pressure, AI-SDK-style + 31 : * inclusive total) without ever subtracting — eliminating the underflow + 32 : * class of bug where a clamped difference would silently store the wrong + 33 : * value. + 34 : * + 35 : * **Semantics by provider**: + 36 : * + 37 : * - OpenAI Chat / Responses / Gemini / Bedrock: provider reports inclusive + 38 : * `inputTokens` and an inclusive `outputTokens`; mapper subtracts to + 39 : * derive the breakdown. + 40 : * - Anthropic: provider reports the breakdown natively (`input_tokens` is + 41 : * non-cached only); mapper sums to derive the inclusive `inputTokens`. + 42 : * Anthropic does *not* break extended-thinking out of `output_tokens`, so + 43 : * `reasoningTokens` is `undefined` and `outputTokens` carries the + 44 : * combined total — a documented limitation of the Anthropic API. + 45 : * + 46 : * `providerMetadata` always carries the provider's raw usage payload — + 47 : * keyed by provider name (`{ openai: ... }`, `{ anthropic: ... }`, etc.) + 48 : * — for fields we don't normalize and for billing-level audit trails. + 49 : * Matches the same escape-hatch field on `LLMEvent`. + 50 : */ + 51 55 : export class Usage extends Schema.Class<Usage>("LLM.Usage")({ + 52 46 : inputTokens: Schema.optional(Schema.Number), + 53 47 : outputTokens: Schema.optional(Schema.Number), + 54 55 : nonCachedInputTokens: Schema.optional(Schema.Number), + 55 55 : cacheReadInputTokens: Schema.optional(Schema.Number), + 56 56 : cacheWriteInputTokens: Schema.optional(Schema.Number), + 57 50 : reasoningTokens: Schema.optional(Schema.Number), + 58 46 : totalTokens: Schema.optional(Schema.Number), + 59 52 : providerMetadata: Schema.optional(ProviderMetadata), + 60 4 : }) { + 61 : /** + 62 : * Visible output tokens — `outputTokens` minus `reasoningTokens`, clamped + 63 : * to zero. The one place subtraction happens in this contract; the clamp + 64 : * means a provider reporting `reasoningTokens > outputTokens` produces a + 65 : * harmless zero rather than a negative that crashes downstream schemas. + 66 : */ + 67 8 : get visibleOutputTokens() { + 68 82 : return Math.max(0, (this.outputTokens ?? 0) - (this.reasoningTokens ?? 0)) + 69 : } + 70 : + 71 13 : static from(input: UsageInput) { + 72 55 : return input instanceof Usage ? input : new Usage(input) + 73 : } + 74 1 : } + 75 : + 76 : export type UsageInput = Usage | ConstructorParameters<typeof Usage>[0] + 77 : + 78 41 : export const StepStart = Schema.Struct({ + 79 33 : type: Schema.tag("step-start"), + 80 21 : index: Schema.Number, + 81 51 : }).annotate({ identifier: "LLM.Event.StepStart" }) + 82 : export type StepStart = Schema.Schema.Type<typeof StepStart> + 83 : + 84 41 : export const TextStart = Schema.Struct({ + 85 33 : type: Schema.tag("text-start"), + 86 21 : id: ContentBlockID, + 87 52 : providerMetadata: Schema.optional(ProviderMetadata), + 88 51 : }).annotate({ identifier: "LLM.Event.TextStart" }) + 89 : export type TextStart = Schema.Schema.Type<typeof TextStart> + 90 : + 91 41 : export const TextDelta = Schema.Struct({ + 92 33 : type: Schema.tag("text-delta"), + 93 21 : id: ContentBlockID, + 94 22 : text: Schema.String, + 95 52 : providerMetadata: Schema.optional(ProviderMetadata), + 96 51 : }).annotate({ identifier: "LLM.Event.TextDelta" }) + 97 : export type TextDelta = Schema.Schema.Type<typeof TextDelta> + 98 : + 99 39 : export const TextEnd = Schema.Struct({ + 100 31 : type: Schema.tag("text-end"), + 101 21 : id: ContentBlockID, + 102 52 : providerMetadata: Schema.optional(ProviderMetadata), + 103 49 : }).annotate({ identifier: "LLM.Event.TextEnd" }) + 104 : export type TextEnd = Schema.Schema.Type<typeof TextEnd> + 105 : + 106 46 : export const ReasoningStart = Schema.Struct({ + 107 38 : type: Schema.tag("reasoning-start"), + 108 21 : id: ContentBlockID, + 109 52 : providerMetadata: Schema.optional(ProviderMetadata), + 110 56 : }).annotate({ identifier: "LLM.Event.ReasoningStart" }) + 111 : export type ReasoningStart = Schema.Schema.Type<typeof ReasoningStart> + 112 : + 113 46 : export const ReasoningDelta = Schema.Struct({ + 114 38 : type: Schema.tag("reasoning-delta"), + 115 21 : id: ContentBlockID, + 116 22 : text: Schema.String, + 117 52 : providerMetadata: Schema.optional(ProviderMetadata), + 118 56 : }).annotate({ identifier: "LLM.Event.ReasoningDelta" }) + 119 : export type ReasoningDelta = Schema.Schema.Type<typeof ReasoningDelta> + 120 : + 121 44 : export const ReasoningEnd = Schema.Struct({ + 122 36 : type: Schema.tag("reasoning-end"), + 123 21 : id: ContentBlockID, + 124 52 : providerMetadata: Schema.optional(ProviderMetadata), + 125 54 : }).annotate({ identifier: "LLM.Event.ReasoningEnd" }) + 126 : export type ReasoningEnd = Schema.Schema.Type<typeof ReasoningEnd> + 127 : + 128 46 : export const ToolInputStart = Schema.Struct({ + 129 39 : type: Schema.tag("tool-input-start"), + 130 17 : id: ToolCallID, + 131 22 : name: Schema.String, + 132 52 : providerMetadata: Schema.optional(ProviderMetadata), + 133 56 : }).annotate({ identifier: "LLM.Event.ToolInputStart" }) + 134 : export type ToolInputStart = Schema.Schema.Type<typeof ToolInputStart> + 135 : + 136 46 : export const ToolInputDelta = Schema.Struct({ + 137 39 : type: Schema.tag("tool-input-delta"), + 138 17 : id: ToolCallID, + 139 22 : name: Schema.String, + 140 20 : text: Schema.String, + 141 56 : }).annotate({ identifier: "LLM.Event.ToolInputDelta" }) + 142 : export type ToolInputDelta = Schema.Schema.Type<typeof ToolInputDelta> + 143 : + 144 44 : export const ToolInputEnd = Schema.Struct({ + 145 37 : type: Schema.tag("tool-input-end"), + 146 17 : id: ToolCallID, + 147 22 : name: Schema.String, + 148 52 : providerMetadata: Schema.optional(ProviderMetadata), + 149 54 : }).annotate({ identifier: "LLM.Event.ToolInputEnd" }) + 150 : export type ToolInputEnd = Schema.Schema.Type<typeof ToolInputEnd> + 151 : + 152 40 : export const ToolCall = Schema.Struct({ + 153 32 : type: Schema.tag("tool-call"), + 154 17 : id: ToolCallID, + 155 22 : name: Schema.String, + 156 24 : input: Schema.Unknown, + 157 52 : providerExecuted: Schema.optional(Schema.Boolean), + 158 52 : providerMetadata: Schema.optional(ProviderMetadata), + 159 50 : }).annotate({ identifier: "LLM.Event.ToolCall" }) + 160 : export type ToolCall = Schema.Schema.Type<typeof ToolCall> + 161 : + 162 42 : export const ToolResult = Schema.Struct({ + 163 34 : type: Schema.tag("tool-result"), + 164 17 : id: ToolCallID, + 165 22 : name: Schema.String, + 166 26 : result: ToolResultValue, + 167 38 : output: Schema.optional(ToolOutput), + 168 52 : providerExecuted: Schema.optional(Schema.Boolean), + 169 52 : providerMetadata: Schema.optional(ProviderMetadata), + 170 52 : }).annotate({ identifier: "LLM.Event.ToolResult" }) + 171 : export type ToolResult = Schema.Schema.Type<typeof ToolResult> + 172 : + 173 41 : export const ToolError = Schema.Struct({ + 174 33 : type: Schema.tag("tool-error"), + 175 17 : id: ToolCallID, + 176 22 : name: Schema.String, + 177 25 : message: Schema.String, + 178 42 : error: Schema.optional(Schema.Defect()), + 179 52 : providerMetadata: Schema.optional(ProviderMetadata), + 180 51 : }).annotate({ identifier: "LLM.Event.ToolError" }) + 181 : export type ToolError = Schema.Schema.Type<typeof ToolError> + 182 : + 183 42 : export const StepFinish = Schema.Struct({ + 184 34 : type: Schema.tag("step-finish"), + 185 23 : index: Schema.Number, + 186 23 : reason: FinishReason, + 187 32 : usage: Schema.optional(Usage), + 188 52 : providerMetadata: Schema.optional(ProviderMetadata), + 189 52 : }).annotate({ identifier: "LLM.Event.StepFinish" }) + 190 : export type StepFinish = Schema.Schema.Type<typeof StepFinish> + 191 : + 192 38 : export const Finish = Schema.Struct({ + 193 29 : type: Schema.tag("finish"), + 194 23 : reason: FinishReason, + 195 32 : usage: Schema.optional(Usage), + 196 52 : providerMetadata: Schema.optional(ProviderMetadata), + 197 48 : }).annotate({ identifier: "LLM.Event.Finish" }) + 198 : export type Finish = Schema.Schema.Type<typeof Finish> + 199 : + 200 50 : export const ProviderErrorEvent = Schema.Struct({ + 201 37 : type: Schema.tag("provider-error"), + 202 25 : message: Schema.String, + 203 65 : classification: Schema.optional(ProviderFailureClassification), + 204 45 : retryable: Schema.optional(Schema.Boolean), + 205 52 : providerMetadata: Schema.optional(ProviderMetadata), + 206 55 : }).annotate({ identifier: "LLM.Event.ProviderError" }) + 207 : export type ProviderErrorEvent = Schema.Schema.Type<typeof ProviderErrorEvent> + 208 : + 209 38 : const llmEventTagged = Schema.Union([ + 210 12 : StepStart, + 211 12 : TextStart, + 212 12 : TextDelta, + 213 10 : TextEnd, + 214 17 : ReasoningStart, + 215 17 : ReasoningDelta, + 216 15 : ReasoningEnd, + 217 17 : ToolInputStart, + 218 17 : ToolInputDelta, + 219 15 : ToolInputEnd, + 220 11 : ToolCall, + 221 13 : ToolResult, + 222 12 : ToolError, + 223 13 : StepFinish, + 224 9 : Finish, + 225 19 : ProviderErrorEvent, + 226 38 : ]).pipe(Schema.toTaggedUnion("type")) + 227 : + 228 : type WithID<Event extends { readonly id: unknown }, ID> = Omit<Event, "type" | "id"> & { readonly id: ID | string } + 229 : type WithUsage<Event extends { readonly usage?: Usage }> = Omit<Event, "type" | "usage"> & { + 230 : readonly usage?: UsageInput + 231 : } + 232 : + 233 59 : const contentBlockID = (value: ContentBlockID | string) => ContentBlockID.make(value) + 234 51 : const toolCallID = (value: ToolCallID | string) => ToolCallID.make(value) + 235 : + 236 : /** + 237 : * camelCase aliases for `LLMEvent.guards` (provided by `Schema.toTaggedUnion`). + 238 : * Lets consumers write `events.filter(LLMEvent.is.toolCall)` instead of + 239 : * `events.filter(LLMEvent.guards["tool-call"])`. + 240 : */ + 241 56 : export const LLMEvent = Object.assign(llmEventTagged, { + 242 28 : stepStart: StepStart.make, + 243 81 : textStart: (input: WithID<TextStart, ContentBlockID>) => TextStart.make({ ...input, id: contentBlockID(input.id) }), + 244 81 : textDelta: (input: WithID<TextDelta, ContentBlockID>) => TextDelta.make({ ...input, id: contentBlockID(input.id) }), + 245 77 : textEnd: (input: WithID<TextEnd, ContentBlockID>) => TextEnd.make({ ...input, id: contentBlockID(input.id) }), + 246 26 : reasoningStart: (input: WithID<ReasoningStart, ContentBlockID>) => + 247 65 : ReasoningStart.make({ ...input, id: contentBlockID(input.id) }), + 248 26 : reasoningDelta: (input: WithID<ReasoningDelta, ContentBlockID>) => + 249 65 : ReasoningDelta.make({ ...input, id: contentBlockID(input.id) }), + 250 24 : reasoningEnd: (input: WithID<ReasoningEnd, ContentBlockID>) => + 251 63 : ReasoningEnd.make({ ...input, id: contentBlockID(input.id) }), + 252 26 : toolInputStart: (input: WithID<ToolInputStart, ToolCallID>) => + 253 61 : ToolInputStart.make({ ...input, id: toolCallID(input.id) }), + 254 26 : toolInputDelta: (input: WithID<ToolInputDelta, ToolCallID>) => + 255 61 : ToolInputDelta.make({ ...input, id: toolCallID(input.id) }), + 256 83 : toolInputEnd: (input: WithID<ToolInputEnd, ToolCallID>) => ToolInputEnd.make({ ...input, id: toolCallID(input.id) }), + 257 75 : toolCall: (input: WithID<ToolCall, ToolCallID>) => ToolCall.make({ ...input, id: toolCallID(input.id) }), + 258 22 : toolResult: (input: WithID<ToolResult, ToolCallID>) => + 259 24 : ToolResult.make({ + 260 10 : ...input, + 261 29 : id: toolCallID(input.id), + 262 110 : output: input.output === undefined ? undefined : ToolOutput.make(input.output.structured, input.output.content), + 263 4 : }), + 264 77 : toolError: (input: WithID<ToolError, ToolCallID>) => ToolError.make({ ...input, id: toolCallID(input.id) }), + 265 22 : stepFinish: (input: WithUsage<StepFinish>) => + 266 24 : StepFinish.make({ + 267 10 : ...input, + 268 69 : usage: input.usage === undefined ? undefined : Usage.from(input.usage), + 269 4 : }), + 270 18 : finish: (input: WithUsage<Finish>) => + 271 20 : Finish.make({ + 272 10 : ...input, + 273 69 : usage: input.usage === undefined ? undefined : Usage.from(input.usage), + 274 4 : }), + 275 41 : providerError: ProviderErrorEvent.make, + 276 9 : is: { + 277 51 : stepStart: llmEventTagged.guards["step-start"], + 278 51 : textStart: llmEventTagged.guards["text-start"], + 279 51 : textDelta: llmEventTagged.guards["text-delta"], + 280 47 : textEnd: llmEventTagged.guards["text-end"], + 281 61 : reasoningStart: llmEventTagged.guards["reasoning-start"], + 282 61 : reasoningDelta: llmEventTagged.guards["reasoning-delta"], + 283 57 : reasoningEnd: llmEventTagged.guards["reasoning-end"], + 284 62 : toolInputStart: llmEventTagged.guards["tool-input-start"], + 285 62 : toolInputDelta: llmEventTagged.guards["tool-input-delta"], + 286 58 : toolInputEnd: llmEventTagged.guards["tool-input-end"], + 287 49 : toolCall: llmEventTagged.guards["tool-call"], + 288 53 : toolResult: llmEventTagged.guards["tool-result"], + 289 51 : toolError: llmEventTagged.guards["tool-error"], + 290 53 : stepFinish: llmEventTagged.guards["step-finish"], + 291 41 : finish: llmEventTagged.guards.finish, + 292 56 : providerError: llmEventTagged.guards["provider-error"], + 293 2 : }, + 294 3 : }) + 295 : export type LLMEvent = Schema.Schema.Type<typeof llmEventTagged> + 296 : + 297 75 : export class PreparedRequest extends Schema.Class<PreparedRequest>("LLM.PreparedRequest")({ + 298 20 : id: Schema.String, + 299 17 : route: RouteID, + 300 23 : protocol: ProtocolID, + 301 21 : model: ModelSchema, + 302 23 : body: Schema.Unknown, + 303 72 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + 304 5 : }) {} + 305 : + 306 : /** + 307 : * A `PreparedRequest` whose `body` is typed as `Body`. Use with the generic + 308 : * on `LLMClient.prepare<Body>(...)` when the caller knows which route their + 309 : * request will resolve to and wants its native shape statically exposed + 310 : * (debug UIs, request previews, plan rendering). + 311 : * + 312 : * The runtime body is identical — the route still emits `body: unknown` — so + 313 : * this is a type-level assertion the caller makes about what they expect to + 314 : * find. The prepare runtime does not validate the assertion. + 315 : */ + 316 : export type PreparedRequestOf<Body> = Omit<PreparedRequest, "body"> & { + 317 : readonly body: Body + 318 : } + 319 : + 320 0 : const responseText = (events: ReadonlyArray<LLMEvent>) => + 321 0 : events + 322 0 : .filter(LLMEvent.is.textDelta) + 323 0 : .map((event) => event.text) + 324 2 : .join("") + 325 : + 326 0 : const responseReasoning = (events: ReadonlyArray<LLMEvent>) => + 327 0 : events + 328 0 : .filter(LLMEvent.is.reasoningDelta) + 329 0 : .map((event) => event.text) + 330 2 : .join("") + 331 : + 332 0 : const responseUsage = (events: ReadonlyArray<LLMEvent>) => + 333 0 : events.reduce<Usage | undefined>( + 334 0 : (usage, event) => ("usage" in event && event.usage !== undefined ? event.usage : usage), + 335 : undefined, + 336 2 : ) + 337 : + 338 : interface ContentAssembly { + 339 : readonly contentIndex: number + 340 : readonly text: string + 341 : readonly providerMetadata?: ProviderMetadata + 342 : } + 343 : + 344 : interface ToolInputAssembly { + 345 : readonly name: string + 346 : readonly text: string + 347 : readonly providerMetadata?: ProviderMetadata + 348 : } + 349 : + 350 : interface ResponseState { + 351 : readonly events: ReadonlyArray<LLMEvent> + 352 : readonly message: Message + 353 : readonly usage?: Usage + 354 : readonly finishReason?: FinishReason + 355 : readonly textParts: Readonly<Record<string, ContentAssembly>> + 356 : readonly reasoningParts: Readonly<Record<string, ContentAssembly>> + 357 : readonly toolInputs: Readonly<Record<string, ToolInputAssembly>> + 358 : } + 359 : + 360 0 : const emptyResponseState = (): ResponseState => ({ + 361 0 : events: [], + 362 0 : message: Message.assistant([]), + 363 0 : textParts: {}, + 364 0 : reasoningParts: {}, + 365 : toolInputs: {}, + 366 2 : }) + 367 : + 368 0 : const appendEvent = (state: ResponseState, event: LLMEvent): ResponseState => { + 369 0 : const events = [...state.events, event] + 370 0 : if (LLMEvent.is.finish(event)) { + 371 0 : return { + 372 0 : ...state, + 373 0 : events, + 374 0 : usage: event.usage ?? state.usage, + 375 0 : finishReason: event.reason, + 376 0 : } + 377 0 : } + 378 0 : if (LLMEvent.is.providerError(event)) { + 379 0 : return { + 380 0 : ...state, + 381 0 : events, + 382 0 : finishReason: state.finishReason ?? "error", + 383 0 : } + 384 0 : } + 385 0 : return { + 386 0 : ...state, + 387 0 : events, + 388 0 : usage: "usage" in event && event.usage !== undefined ? event.usage : state.usage, + 389 2 : } + 390 : } + 391 : + 392 0 : const textContent = (text: string, providerMetadata: ProviderMetadata | undefined): ContentPart => + 393 2 : providerMetadata === undefined ? { type: "text", text } : { type: "text", text, providerMetadata } + 394 : + 395 0 : const reasoningContent = (text: string, providerMetadata: ProviderMetadata | undefined): ContentPart => + 396 2 : providerMetadata === undefined ? { type: "reasoning", text } : { type: "reasoning", text, providerMetadata } + 397 : + 398 0 : const contentWith = (state: ResponseState, content: ReadonlyArray<ContentPart>): ResponseState => ({ + 399 0 : ...state, + 400 : message: Message.assistant(content), + 401 2 : }) + 402 : + 403 22 : const appendContent = (state: ResponseState, part: ContentPart) => contentWith(state, [...state.message.content, part]) + 404 : + 405 0 : const replaceContent = (state: ResponseState, index: number, part: ContentPart) => + 406 0 : contentWith( + 407 0 : state, + 408 : state.message.content.map((item, itemIndex) => (itemIndex === index ? part : item)), + 409 2 : ) + 410 : + 411 0 : const ensureText = (state: ResponseState, id: string, providerMetadata?: ProviderMetadata): ResponseState => { + 412 0 : if (state.textParts[id]) return state + 413 0 : return { + 414 0 : ...appendContent(state, textContent("", providerMetadata)), + 415 0 : textParts: { + 416 0 : ...state.textParts, + 417 0 : [id]: { contentIndex: state.message.content.length, text: "", providerMetadata }, + 418 0 : }, + 419 2 : } + 420 : } + 421 : + 422 0 : const reduceTextDelta = (state: ResponseState, event: TextDelta): ResponseState => { + 423 0 : const started = ensureText(state, event.id, event.providerMetadata) + 424 0 : const current = started.textParts[event.id] + 425 0 : if (!current) return started + 426 0 : const text = current.text + event.text + 427 0 : const providerMetadata = event.providerMetadata ?? current.providerMetadata + 428 0 : return { + 429 0 : ...replaceContent(started, current.contentIndex, textContent(text, providerMetadata)), + 430 0 : textParts: { ...started.textParts, [event.id]: { ...current, text, providerMetadata } }, + 431 2 : } + 432 : } + 433 : + 434 0 : const reduceTextEnd = (state: ResponseState, event: TextEnd): ResponseState => { + 435 0 : const current = state.textParts[event.id] + 436 0 : if (!current) return state + 437 0 : const providerMetadata = event.providerMetadata ?? current.providerMetadata + 438 0 : return { + 439 0 : ...replaceContent(state, current.contentIndex, textContent(current.text, providerMetadata)), + 440 0 : textParts: { ...state.textParts, [event.id]: { ...current, providerMetadata } }, + 441 2 : } + 442 : } + 443 : + 444 0 : const ensureReasoning = (state: ResponseState, id: string, providerMetadata?: ProviderMetadata): ResponseState => { + 445 0 : if (state.reasoningParts[id]) return state + 446 0 : return { + 447 0 : ...appendContent(state, reasoningContent("", providerMetadata)), + 448 0 : reasoningParts: { + 449 0 : ...state.reasoningParts, + 450 0 : [id]: { contentIndex: state.message.content.length, text: "", providerMetadata }, + 451 0 : }, + 452 2 : } + 453 : } + 454 : + 455 0 : const reduceReasoningDelta = (state: ResponseState, event: ReasoningDelta): ResponseState => { + 456 0 : const started = ensureReasoning(state, event.id, event.providerMetadata) + 457 0 : const current = started.reasoningParts[event.id] + 458 0 : if (!current) return started + 459 0 : const text = current.text + event.text + 460 0 : const providerMetadata = event.providerMetadata ?? current.providerMetadata + 461 0 : return { + 462 0 : ...replaceContent(started, current.contentIndex, reasoningContent(text, providerMetadata)), + 463 0 : reasoningParts: { ...started.reasoningParts, [event.id]: { ...current, text, providerMetadata } }, + 464 2 : } + 465 : } + 466 : + 467 0 : const reduceReasoningEnd = (state: ResponseState, event: ReasoningEnd): ResponseState => { + 468 0 : const current = state.reasoningParts[event.id] + 469 0 : if (!current) return state + 470 0 : const providerMetadata = event.providerMetadata ?? current.providerMetadata + 471 0 : return { + 472 0 : ...replaceContent(state, current.contentIndex, reasoningContent(current.text, providerMetadata)), + 473 0 : reasoningParts: { ...state.reasoningParts, [event.id]: { ...current, providerMetadata } }, + 474 2 : } + 475 : } + 476 : + 477 0 : const reduceToolInputStart = (state: ResponseState, event: ToolInputStart): ResponseState => ({ + 478 0 : ...state, + 479 0 : toolInputs: { + 480 0 : ...state.toolInputs, + 481 0 : [event.id]: { name: event.name, text: "", providerMetadata: event.providerMetadata }, + 482 : }, + 483 2 : }) + 484 : + 485 0 : const reduceToolInputDelta = (state: ResponseState, event: ToolInputDelta): ResponseState => { + 486 0 : const current = state.toolInputs[event.id] ?? { name: event.name, text: "" } + 487 0 : return { + 488 0 : ...state, + 489 0 : toolInputs: { ...state.toolInputs, [event.id]: { ...current, text: current.text + event.text } }, + 490 2 : } + 491 : } + 492 : + 493 0 : const reduceToolInputEnd = (state: ResponseState, event: ToolInputEnd): ResponseState => { + 494 0 : const current = state.toolInputs[event.id] ?? { name: event.name, text: "" } + 495 0 : return { + 496 0 : ...state, + 497 0 : toolInputs: { + 498 0 : ...state.toolInputs, + 499 0 : [event.id]: { + 500 0 : ...current, + 501 0 : name: event.name, + 502 0 : providerMetadata: event.providerMetadata ?? current.providerMetadata, + 503 0 : }, + 504 0 : }, + 505 2 : } + 506 : } + 507 : + 508 0 : const toolCallContent = (event: ToolCall): ContentPart => + 509 0 : ToolCallPart.make({ + 510 0 : id: event.id, + 511 0 : name: event.name, + 512 0 : input: event.input, + 513 0 : ...(event.providerExecuted === undefined ? {} : { providerExecuted: event.providerExecuted }), + 514 : ...(event.providerMetadata === undefined ? {} : { providerMetadata: event.providerMetadata }), + 515 2 : }) + 516 : + 517 0 : const toolResultContent = (event: ToolResult): ContentPart => + 518 0 : ToolResultPart.make({ + 519 0 : id: event.id, + 520 0 : name: event.name, + 521 0 : result: event.result, + 522 0 : ...(event.providerExecuted === undefined ? {} : { providerExecuted: event.providerExecuted }), + 523 : ...(event.providerMetadata === undefined ? {} : { providerMetadata: event.providerMetadata }), + 524 2 : }) + 525 : + 526 0 : const reduceToolCall = (state: ResponseState, event: ToolCall): ResponseState => { + 527 0 : const { [event.id]: _finished, ...toolInputs } = state.toolInputs + 528 2 : return { ...appendContent(state, toolCallContent(event)), toolInputs } + 529 : } + 530 : + 531 0 : const reduceResponseState = (state: ResponseState, event: LLMEvent): ResponseState => { + 532 0 : const next = appendEvent(state, event) + 533 0 : switch (event.type) { + 534 0 : case "text-start": + 535 0 : return ensureText(next, event.id, event.providerMetadata) + 536 0 : case "text-delta": + 537 0 : return reduceTextDelta(next, event) + 538 0 : case "text-end": + 539 0 : return reduceTextEnd(next, event) + 540 0 : case "reasoning-start": + 541 0 : return ensureReasoning(next, event.id, event.providerMetadata) + 542 0 : case "reasoning-delta": + 543 0 : return reduceReasoningDelta(next, event) + 544 0 : case "reasoning-end": + 545 0 : return reduceReasoningEnd(next, event) + 546 0 : case "tool-input-start": + 547 0 : return reduceToolInputStart(next, event) + 548 0 : case "tool-input-delta": + 549 0 : return reduceToolInputDelta(next, event) + 550 0 : case "tool-input-end": + 551 0 : return reduceToolInputEnd(next, event) + 552 0 : case "tool-call": + 553 0 : return reduceToolCall(next, event) + 554 0 : case "tool-result": + 555 0 : return appendContent(next, toolResultContent(event)) + 556 0 : default: + 557 2 : return next + 558 : } + 559 : } + 560 : + 561 64 : export class LLMResponse extends Schema.Class<LLMResponse>("LLM.Response")({ + 562 19 : message: Message, + 563 33 : events: Schema.Array(LLMEvent), + 564 32 : usage: Schema.optional(Usage), + 565 27 : finishReason: FinishReason, + 566 0 : }) { + 567 0 : /** Concatenated assistant text assembled from streamed `text-delta` events. */ + 568 0 : get text() { + 569 0 : return responseText(this.events) + 570 0 : } + 571 0 : + 572 0 : /** Concatenated reasoning text assembled from streamed `reasoning-delta` events. */ + 573 0 : get reasoning() { + 574 0 : return responseReasoning(this.events) + 575 0 : } + 576 0 : + 577 0 : /** Completed tool calls emitted by the provider. */ + 578 0 : get toolCalls() { + 579 1 : return this.events.filter(LLMEvent.is.toolCall) + 580 : } + 581 1 : } + 582 : + 583 40 : export namespace LLMResponse { + 584 : export type State = ResponseState + 585 : export type Output = LLMResponse | { readonly events: ReadonlyArray<LLMEvent>; readonly usage?: Usage } + 586 : + 587 : /** Initial reducer state for assembling one provider attempt. */ + 588 41 : export const empty = emptyResponseState + 589 : + 590 : /** Purely fold one provider-neutral event into the attempt assembly state. */ + 591 43 : export const reduce = reduceResponseState + 592 : + 593 : /** Return a completed response only after a terminal finish or provider error. */ + 594 0 : export const complete = (state: State): LLMResponse | undefined => + 595 0 : state.finishReason === undefined + 596 0 : ? undefined + 597 0 : : new LLMResponse({ + 598 0 : message: state.message, + 599 0 : events: [...state.events], + 600 0 : usage: state.usage, + 601 0 : finishReason: state.finishReason, + 602 3 : }) + 603 : + 604 : /** Convenience reducer for callers that already have a collected event list. */ + 605 27 : export const fromEvents = (events: ReadonlyArray<LLMEvent>) => complete(events.reduce(reduce, empty())) + 606 : + 607 : /** Concatenate assistant text from a response or collected event list. */ + 608 21 : export const text = (response: Output) => responseText(response.events) + 609 : + 610 : /** Return response usage, falling back to the latest usage-bearing event. */ + 611 22 : export const usage = (response: Output) => response.usage ?? responseUsage(response.events) + 612 : + 613 : /** Return completed tool calls from a response or collected event list. */ + 614 26 : export const toolCalls = (response: Output) => response.events.filter(LLMEvent.is.toolCall) + 615 : + 616 : /** Concatenate reasoning text from a response or collected event list. */ + 617 27 : export const reasoning = (response: Output) => responseReasoning(response.events) + 618 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Schema } from "effect" + 2 59 : import { ProviderMetadata } from "@opencode-ai/schema/llm" + 3 : + 4 28 : export { ProviderMetadata } + 5 : + 6 : /** Stable string identifier for a protocol implementation. */ + 7 40 : export const ProtocolID = Schema.String + 8 : export type ProtocolID = Schema.Schema.Type<typeof ProtocolID> + 9 : + 10 : /** Stable string identifier for the runnable route. */ + 11 37 : export const RouteID = Schema.String + 12 : export type RouteID = Schema.Schema.Type<typeof RouteID> + 13 : + 14 71 : export const ModelID = Schema.String.pipe(Schema.brand("LLM.ModelID")) + 15 : export type ModelID = typeof ModelID.Type + 16 : + 17 77 : export const ProviderID = Schema.String.pipe(Schema.brand("LLM.ProviderID")) + 18 : export type ProviderID = typeof ProviderID.Type + 19 : + 20 40 : export const ResponseID = Schema.String + 21 : export type ResponseID = Schema.Schema.Type<typeof ResponseID> + 22 : + 23 44 : export const ContentBlockID = Schema.String + 24 : export type ContentBlockID = Schema.Schema.Type<typeof ContentBlockID> + 25 : + 26 40 : export const ToolCallID = Schema.String + 27 : export type ToolCallID = Schema.Schema.Type<typeof ToolCallID> + 28 : + 29 93 : export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const + 30 65 : export const ReasoningEffort = Schema.Literals(ReasoningEfforts) + 31 : export type ReasoningEffort = Schema.Schema.Type<typeof ReasoningEffort> + 32 : + 33 72 : export const TextVerbosity = Schema.Literals(["low", "medium", "high"]) + 34 : export type TextVerbosity = Schema.Schema.Type<typeof TextVerbosity> + 35 : + 36 84 : export const MessageRole = Schema.Literals(["system", "user", "assistant", "tool"]) + 37 : export type MessageRole = Schema.Schema.Type<typeof MessageRole> + 38 : + 39 116 : export const FinishReason = Schema.Literals(["stop", "length", "tool-calls", "content-filter", "error", "unknown"]) + 40 : export type FinishReason = Schema.Schema.Type<typeof FinishReason> + 41 : + 42 70 : export const JsonSchema = Schema.Record(Schema.String, Schema.Unknown) + 43 : export type JsonSchema = Schema.Schema.Type<typeof JsonSchema> ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| errors.ts | +
+ |
+ 78.4 % | +167 | +131 | +|
| events.ts | +
+ |
+ 57.6 % | +415 | +239 | +|
| ids.ts | +
+ |
+ 100.0 % | +16 | +16 | +|
| index.ts | +
+ |
+ 100.0 % | +5 | +5 | +|
| messages.ts | +
+ |
+ 96.4 % | +223 | +215 | +|
| options.ts | +
+ |
+ 87.2 % | +164 | +143 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| events.ts | +
+ |
+ 57.6 % | +415 | +239 | +|
| errors.ts | +
+ |
+ 78.4 % | +167 | +131 | +|
| options.ts | +
+ |
+ 87.2 % | +164 | +143 | +|
| messages.ts | +
+ |
+ 96.4 % | +223 | +215 | +|
| index.ts | +
+ |
+ 100.0 % | +5 | +5 | +|
| ids.ts | +
+ |
+ 100.0 % | +16 | +16 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| errors.ts | +
+ |
+ 78.4 % | +167 | +131 | +|
| events.ts | +
+ |
+ 57.6 % | +415 | +239 | +|
| ids.ts | +
+ |
+ 100.0 % | +16 | +16 | +|
| index.ts | +
+ |
+ 100.0 % | +5 | +5 | +|
| messages.ts | +
+ |
+ 96.4 % | +223 | +215 | +|
| options.ts | +
+ |
+ 87.2 % | +164 | +143 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 22 : export * from "./ids" + 2 26 : export * from "./options" + 3 27 : export * from "./messages" + 4 25 : export * from "./events" + 5 24 : export * from "./errors" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 0 : import { Schema } from "effect" + 2 83 : import { ToolContent, ToolFileContent, ToolTextContent } from "@opencode-ai/schema/llm" + 3 66 : import { JsonSchema, MessageRole, ProviderMetadata } from "./ids" + 4 113 : import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options" + 5 43 : import { isRecord } from "../utils/record" + 6 : + 7 41 : const systemPartSchema = Schema.Struct({ + 8 31 : type: Schema.Literal("text"), + 9 22 : text: Schema.String, + 10 36 : cache: Schema.optional(CacheHint), + 11 72 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + 12 46 : }).annotate({ identifier: "LLM.SystemPart" }) + 13 : export type SystemPart = Schema.Schema.Type<typeof systemPartSchema> + 14 : + 15 56 : const makeSystemPart = (text: string): SystemPart => ({ type: "text", text }) + 16 : + 17 60 : export const SystemPart = Object.assign(systemPartSchema, { + 18 23 : make: makeSystemPart, + 19 24 : content: (input?: string | SystemPart | ReadonlyArray<SystemPart>) => { + 20 42 : if (input === undefined) return [] + 21 69 : return typeof input === "string" ? [makeSystemPart(input)] : Array.isArray(input) ? [...input] : [input] + 22 : }, + 23 3 : }) + 24 : + 25 40 : export const TextPart = Schema.Struct({ + 26 31 : type: Schema.Literal("text"), + 27 22 : text: Schema.String, + 28 36 : cache: Schema.optional(CacheHint), + 29 74 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + 30 52 : providerMetadata: Schema.optional(ProviderMetadata), + 31 48 : }).annotate({ identifier: "LLM.Content.Text" }) + 32 : export type TextPart = Schema.Schema.Type<typeof TextPart> + 33 : + 34 41 : export const MediaPart = Schema.Struct({ + 35 32 : type: Schema.Literal("media"), + 36 27 : mediaType: Schema.String, + 37 57 : data: Schema.Union([Schema.String, Schema.Uint8Array]), + 38 43 : filename: Schema.optional(Schema.String), + 39 72 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + 40 49 : }).annotate({ identifier: "LLM.Content.Media" }) + 41 : export type MediaPart = Schema.Schema.Type<typeof MediaPart> + 42 : + 43 57 : export { ToolContent, ToolFileContent, ToolTextContent } + 44 : + 45 35 : const isToolResultValue = (value: unknown): value is ToolResultValue => + 46 20 : isRecord(value) && + 47 106 : (value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") && + 48 18 : "value" in value + 49 : + 50 44 : export const ToolResultValue = Object.assign( + 51 16 : Schema.Union([ + 52 19 : Schema.Struct({ + 53 33 : type: Schema.Literal("json"), + 54 23 : value: Schema.Unknown, + 55 5 : }), + 56 19 : Schema.Struct({ + 57 33 : type: Schema.Literal("text"), + 58 23 : value: Schema.Unknown, + 59 5 : }), + 60 19 : Schema.Struct({ + 61 34 : type: Schema.Literal("error"), + 62 23 : value: Schema.Unknown, + 63 5 : }), + 64 19 : Schema.Struct({ + 65 36 : type: Schema.Literal("content"), + 66 34 : value: Schema.Array(ToolContent), + 67 3 : }), + 68 46 : ]).annotate({ identifier: "LLM.ToolResult" }), + 69 3 : { + 70 24 : is: isToolResultValue, + 71 36 : make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => { + 72 50 : if (isToolResultValue(value)) return value + 73 32 : if (type === "content") return { type, value: Array.isArray(value) ? value : [] } + 74 23 : return { type, value } + 75 : }, + 76 : }, + 77 3 : ) + 78 : export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue> + 79 : + 80 : export interface ToolOutput { + 81 : readonly structured: unknown + 82 : readonly content: ReadonlyArray<ToolContent> + 83 : } + 84 : + 85 39 : export const ToolOutput = Object.assign( + 86 17 : Schema.Struct({ + 87 29 : structured: Schema.Unknown, + 88 35 : content: Schema.Array(ToolContent), + 89 46 : }).annotate({ identifier: "LLM.ToolOutput" }), + 90 3 : { + 91 62 : make: (structured: unknown, content: ReadonlyArray<ToolContent> = []): ToolOutput => ({ structured, content }), + 92 33 : fromResultValue: (result: ToolResultValue): ToolOutput | undefined => { + 93 32 : switch (result.type) { + 94 15 : case "json": + 95 48 : return { structured: result.value, content: [] } + 96 0 : case "text": + 97 5 : return { structured: {}, content: [{ type: "text", text: toolResultText(result.value) }] } + 98 18 : case "content": + 99 48 : return { structured: {}, content: result.value } + 100 0 : case "error": + 101 3 : return undefined + 102 : } + 103 : }, + 104 31 : toResultValue: (output: ToolOutput): ToolResultValue => { + 105 90 : if (output.content.length === 0) return { type: "json", value: output.structured } + 106 75 : if (output.content.length === 1 && output.content[0]?.type === "text") + 107 58 : return { type: "text", value: output.content[0].text } + 108 50 : return { type: "content", value: output.content } + 109 : }, + 110 : }, + 111 3 : ) + 112 : + 113 0 : const toolResultText = (value: unknown) => { + 114 0 : if (typeof value === "string") return value + 115 0 : try { + 116 0 : return JSON.stringify(value) ?? String(value) + 117 0 : } catch { + 118 2 : return String(value) + 119 : } + 120 : } + 121 : + 122 41 : export const ToolCallPart = Object.assign( + 123 17 : Schema.Struct({ + 124 36 : type: Schema.Literal("tool-call"), + 125 20 : id: Schema.String, + 126 22 : name: Schema.String, + 127 24 : input: Schema.Unknown, + 128 52 : providerExecuted: Schema.optional(Schema.Boolean), + 129 74 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + 130 52 : providerMetadata: Schema.optional(ProviderMetadata), + 131 52 : }).annotate({ identifier: "LLM.Content.ToolCall" }), + 132 3 : { + 133 49 : make: (input: Omit<ToolCallPart, "type">): ToolCallPart => ({ type: "tool-call", ...input }), + 134 : }, + 135 3 : ) + 136 : export type ToolCallPart = Schema.Schema.Type<typeof ToolCallPart> + 137 : + 138 43 : export const ToolResultPart = Object.assign( + 139 17 : Schema.Struct({ + 140 38 : type: Schema.Literal("tool-result"), + 141 20 : id: Schema.String, + 142 22 : name: Schema.String, + 143 26 : result: ToolResultValue, + 144 52 : providerExecuted: Schema.optional(Schema.Boolean), + 145 36 : cache: Schema.optional(CacheHint), + 146 74 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + 147 52 : providerMetadata: Schema.optional(ProviderMetadata), + 148 54 : }).annotate({ identifier: "LLM.Content.ToolResult" }), + 149 3 : { + 150 6 : make: ( + 151 11 : input: Omit<ToolResultPart, "type" | "result"> & { + 152 : readonly result: unknown + 153 : readonly resultType?: ToolResultValue["type"] + 154 : }, + 155 5 : ): ToolResultPart => ({ + 156 24 : type: "tool-result", + 157 17 : id: input.id, + 158 21 : name: input.name, + 159 65 : result: ToolResultValue.make(input.result, input.resultType), + 160 45 : providerExecuted: input.providerExecuted, + 161 23 : cache: input.cache, + 162 29 : metadata: input.metadata, + 163 42 : providerMetadata: input.providerMetadata, + 164 2 : }), + 165 : }, + 166 3 : ) + 167 : export type ToolResultPart = Schema.Schema.Type<typeof ToolResultPart> + 168 : + 169 45 : export const ReasoningPart = Schema.Struct({ + 170 36 : type: Schema.Literal("reasoning"), + 171 22 : text: Schema.String, + 172 44 : encrypted: Schema.optional(Schema.String), + 173 74 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + 174 52 : providerMetadata: Schema.optional(ProviderMetadata), + 175 53 : }).annotate({ identifier: "LLM.Content.Reasoning" }) + 176 : export type ReasoningPart = Schema.Schema.Type<typeof ReasoningPart> + 177 : + 178 112 : export const ContentPart = Schema.Union([TextPart, MediaPart, ToolCallPart, ToolResultPart, ReasoningPart]).pipe( + 179 28 : Schema.toTaggedUnion("type"), + 180 3 : ) + 181 : export type ContentPart = Schema.Schema.Type<typeof ContentPart> + 182 : + 183 59 : export class Message extends Schema.Class<Message>("LLM.Message")({ + 184 37 : id: Schema.optional(Schema.String), + 185 20 : role: MessageRole, + 186 37 : content: Schema.Array(ContentPart), + 187 74 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + 188 70 : native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + 189 5 : }) {} + 190 : + 191 33 : export namespace Message { + 192 : export type ContentInput = string | ContentPart | ReadonlyArray<ContentPart> + 193 : export type SystemContentInput = string | TextPart | ReadonlyArray<TextPart> + 194 : export type Input = Omit<ConstructorParameters<typeof Message>[0], "content"> & { + 195 : readonly content: ContentInput + 196 : } + 197 : + 198 58 : export const text = (value: string): ContentPart => ({ type: "text", text: value }) + 199 : + 200 28 : export const content = (input: ContentInput) => + 201 85 : typeof input === "string" ? [text(input)] : Array.isArray(input) ? [...input] : [input] + 202 : + 203 30 : export const make = (input: Message | Input) => { + 204 50 : if (input instanceof Message) return input + 205 76 : return new Message({ ...input, content: content(input.content) }) + 206 : } + 207 : + 208 68 : export const user = (content: ContentInput) => make({ role: "user", content }) + 209 : + 210 78 : export const assistant = (content: ContentInput) => make({ role: "assistant", content }) + 211 : + 212 : /** + 213 : * Add an operator-authored instruction at this chronological point in the + 214 : * conversation. This is distinct from the initial `LLMRequest.system` + 215 : * prompt. Keep raw retrieved, tool, and web content out of privileged system + 216 : * updates; pass that untrusted content through ordinary user/tool channels. + 217 : */ + 218 72 : export const system = (content: SystemContentInput) => make({ role: "system", content }) + 219 : + 220 26 : export const tool = (result: ToolResultPart | Parameters<typeof ToolResultPart.make>[0]) => + 221 69 : make({ role: "tool", content: ["type" in result ? result : ToolResultPart.make(result)] }) + 222 : } + 223 : + 224 73 : export class ToolDefinition extends Schema.Class<ToolDefinition>("LLM.ToolDefinition")({ + 225 22 : name: Schema.String, + 226 29 : description: Schema.String, + 227 26 : inputSchema: JsonSchema, + 228 44 : outputSchema: Schema.optional(JsonSchema), + 229 36 : cache: Schema.optional(CacheHint), + 230 74 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + 231 70 : native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + 232 5 : }) {} + 233 : + 234 47 : export namespace ToolDefinition { + 235 : export type Input = ToolDefinition | ConstructorParameters<typeof ToolDefinition>[0] + 236 : + 237 : /** Normalize tool definition input into the canonical `ToolDefinition` class. */ + 238 74 : export const make = (input: Input) => (input instanceof ToolDefinition ? input : new ToolDefinition(input)) + 239 : } + 240 : + 241 65 : export class ToolChoice extends Schema.Class<ToolChoice>("LLM.ToolChoice")({ + 242 62 : type: Schema.Literals(["auto", "none", "required", "tool"]), + 243 37 : name: Schema.optional(Schema.String), + 244 5 : }) {} + 245 : + 246 39 : export namespace ToolChoice { + 247 : export type Mode = Exclude<ToolChoice["type"], "tool"> + 248 : export type Input = ToolChoice | ConstructorParameters<typeof ToolChoice>[0] | ToolDefinition | string + 249 : + 250 87 : const isMode = (value: string): value is Mode => value === "auto" || value === "none" || value === "required" + 251 : + 252 : /** Select a specific named tool. */ + 253 21 : export const named = (value: string) => new ToolChoice({ type: "tool", name: value }) + 254 : + 255 : /** Normalize ergonomic tool-choice inputs into the canonical `ToolChoice` class. */ + 256 33 : export const make = (input: Input) => { + 257 41 : if (input instanceof ToolChoice) return input + 258 45 : if (input instanceof ToolDefinition) return named(input.name) + 259 88 : if (typeof input === "string") return isMode(input) ? new ToolChoice({ type: input }) : named(input) + 260 4 : return new ToolChoice(input) + 261 : } + 262 : } + 263 : + 264 45 : export const ResponseFormat = Schema.Union([ + 265 50 : Schema.Struct({ type: Schema.Literal("text") }), + 266 70 : Schema.Struct({ type: Schema.Literal("json"), schema: JsonSchema }), + 267 70 : Schema.Struct({ type: Schema.Literal("tool"), tool: ToolDefinition }), + 268 38 : ]).pipe(Schema.toTaggedUnion("type")) + 269 : export type ResponseFormat = Schema.Schema.Type<typeof ResponseFormat> + 270 : + 271 62 : export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({ + 272 37 : id: Schema.optional(Schema.String), + 273 21 : model: ModelSchema, + 274 35 : system: Schema.Array(SystemPart), + 275 34 : messages: Schema.Array(Message), + 276 38 : tools: Schema.Array(ToolDefinition), + 277 42 : toolChoice: Schema.optional(ToolChoice), + 278 49 : generation: Schema.optional(GenerationOptions), + 279 52 : providerOptions: Schema.optional(ProviderOptions), + 280 37 : http: Schema.optional(HttpOptions), + 281 50 : responseFormat: Schema.optional(ResponseFormat), + 282 38 : cache: Schema.optional(CachePolicy), + 283 72 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + 284 5 : }) {} + 285 : + 286 38 : export namespace LLMRequest { + 287 : export type Input = ConstructorParameters<typeof LLMRequest>[0] + 288 : + 289 37 : export const input = (request: LLMRequest): Input => ({ + 290 19 : id: request.id, + 291 25 : model: request.model, + 292 27 : system: request.system, + 293 31 : messages: request.messages, + 294 25 : tools: request.tools, + 295 35 : toolChoice: request.toolChoice, + 296 35 : generation: request.generation, + 297 45 : providerOptions: request.providerOptions, + 298 23 : http: request.http, + 299 43 : responseFormat: request.responseFormat, + 300 25 : cache: request.cache, + 301 28 : metadata: request.metadata, + 302 4 : }) + 303 : + 304 44 : export const update = (request: LLMRequest, patch: Partial<Input>) => { + 305 45 : if (Object.keys(patch).length === 0) return request + 306 32 : return new LLMRequest({ + 307 35 : ...input(request), + 308 12 : ...patch, + 309 39 : model: patch.model ?? request.model, + 310 6 : }) + 311 : } + 312 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 64 : import { Schema } from "effect" + 2 112 : import { JsonSchema, ModelID, ProviderID } from "./ids" + 3 : import type { AnyRoute } from "../route/client" + 4 86 : import { isRecord } from "../utils/record" + 5 : + 6 69 : export const mergeJsonRecords = ( + 7 20 : ...items: ReadonlyArray<Record<string, unknown> | undefined> + 8 6 : ): Record<string, unknown> | undefined => { + 9 120 : const defined = items.filter((item): item is Record<string, unknown> => item !== undefined) + 10 74 : if (defined.length === 0) return undefined + 11 228 : if (defined.length === 1 && Object.values(defined[0]).every((value) => value !== undefined)) return defined[0] + 12 40 : const result: Record<string, unknown> = {} + 13 65 : for (const item of defined) { + 14 111 : for (const [key, value] of Object.entries(item)) { + 15 92 : if (value === undefined) continue + 16 173 : result[key] = isRecord(result[key]) && isRecord(value) ? mergeJsonRecords(result[key], value) : value + 17 5 : } + 18 5 : } + 19 115 : return Object.keys(result).length === 0 ? undefined : result + 20 : } + 21 : + 22 59 : const mergeStringRecords = ( + 23 20 : ...items: ReadonlyArray<Record<string, string> | undefined> + 24 6 : ): Record<string, string> | undefined => { + 25 120 : const defined = items.filter((item): item is Record<string, string> => item !== undefined) + 26 74 : if (defined.length === 0) return undefined + 27 96 : if (defined.length === 1) return defined[0] + 28 68 : const result = Object.fromEntries( + 29 51 : defined.flatMap((item) => + 30 121 : Object.entries(item).filter((entry): entry is [string, string] => entry[1] !== undefined), + 31 2 : ), + 32 8 : ) + 33 115 : return Object.keys(result).length === 0 ? undefined : result + 34 : } + 35 : + 36 212 : export const ProviderOptions = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)) + 37 : export type ProviderOptions = Schema.Schema.Type<typeof ProviderOptions> + 38 : + 39 77 : export const mergeProviderOptions = ( + 40 20 : ...items: ReadonlyArray<ProviderOptions | undefined> + 41 6 : ): ProviderOptions | undefined => { + 42 40 : const result: Record<string, Record<string, unknown>> = {} + 43 61 : for (const item of items) { + 44 56 : if (!item) continue + 45 125 : for (const [provider, options] of Object.entries(item)) { + 46 130 : const merged = mergeJsonRecords(result[provider], options) + 47 96 : if (merged) result[provider] = merged + 48 5 : } + 49 5 : } + 50 124 : return Object.keys(result).length === 0 ? undefined : result + 51 : } + 52 : + 53 134 : export class HttpOptions extends Schema.Class<HttpOptions>("LLM.HttpOptions")({ + 54 72 : body: Schema.optional(JsonSchema), + 55 144 : headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), + 56 136 : query: Schema.optional(Schema.Record(Schema.String, Schema.String)), + 57 10 : }) {} + 58 : + 59 82 : export namespace HttpOptions { + 60 : export type Input = HttpOptions | ConstructorParameters<typeof HttpOptions>[0] + 61 : + 62 : /** Normalize HTTP option input into the canonical `HttpOptions` class. */ + 63 182 : export const make = (input: Input) => (input instanceof HttpOptions ? input : new HttpOptions(input)) + 64 : } + 65 : + 66 95 : export const mergeHttpOptions = (...items: ReadonlyArray<HttpOptions | undefined>): HttpOptions | undefined => { + 67 134 : const body = mergeJsonRecords(...items.map((item) => item?.body)) + 68 150 : const headers = mergeStringRecords(...items.map((item) => item?.headers)) + 69 142 : const query = mergeStringRecords(...items.map((item) => item?.query)) + 70 88 : if (!body && !headers && !query) return undefined + 71 102 : return new HttpOptions({ body, headers, query }) + 72 : } + 73 : + 74 158 : export class GenerationOptions extends Schema.Class<GenerationOptions>("LLM.GenerationOptions")({ + 75 88 : maxTokens: Schema.optional(Schema.Number), + 76 92 : temperature: Schema.optional(Schema.Number), + 77 78 : topP: Schema.optional(Schema.Number), + 78 78 : topK: Schema.optional(Schema.Number), + 79 102 : frequencyPenalty: Schema.optional(Schema.Number), + 80 100 : presencePenalty: Schema.optional(Schema.Number), + 81 78 : seed: Schema.optional(Schema.Number), + 82 102 : stop: Schema.optional(Schema.Array(Schema.String)), + 83 10 : }) {} + 84 : + 85 106 : export namespace GenerationOptions { + 86 : export type Input = GenerationOptions | ConstructorParameters<typeof GenerationOptions>[0] + 87 : + 88 : /** Normalize generation option input into the canonical `GenerationOptions` class. */ + 89 228 : export const make = (input: Input = {}) => (input instanceof GenerationOptions ? input : new GenerationOptions(input)) + 90 : } + 91 : + 92 : export type GenerationOptionsFields = { + 93 : readonly maxTokens?: number + 94 : readonly temperature?: number + 95 : readonly topP?: number + 96 : readonly topK?: number + 97 : readonly frequencyPenalty?: number + 98 : readonly presencePenalty?: number + 99 : readonly seed?: number + 100 : readonly stop?: ReadonlyArray<string> + 101 : } + 102 : + 103 : export type GenerationOptionsInput = GenerationOptions | GenerationOptionsFields + 104 : + 105 49 : const latestGeneration = <Key extends keyof GenerationOptionsFields>( + 106 14 : items: ReadonlyArray<GenerationOptionsInput | undefined>, + 107 16 : key: Key, + 108 117 : ) => items.findLast((item) => item?.[key] !== undefined)?.[key] + 109 : + 110 107 : export const mergeGenerationOptions = (...items: ReadonlyArray<GenerationOptionsInput | undefined>) => { + 111 84 : const result = new GenerationOptions({ + 112 104 : maxTokens: latestGeneration(items, "maxTokens"), + 113 112 : temperature: latestGeneration(items, "temperature"), + 114 84 : topP: latestGeneration(items, "topP"), + 115 84 : topK: latestGeneration(items, "topK"), + 116 132 : frequencyPenalty: latestGeneration(items, "frequencyPenalty"), + 117 128 : presencePenalty: latestGeneration(items, "presencePenalty"), + 118 84 : seed: latestGeneration(items, "seed"), + 119 78 : stop: latestGeneration(items, "stop"), + 120 10 : }) + 121 174 : return Object.values(result).some((value) => value !== undefined) ? result : undefined + 122 : } + 123 : + 124 134 : export class ModelLimits extends Schema.Class<ModelLimits>("LLM.ModelLimits")({ + 125 84 : context: Schema.optional(Schema.Number), + 126 78 : output: Schema.optional(Schema.Number), + 127 10 : }) {} + 128 : + 129 82 : export namespace ModelLimits { + 130 : export type Input = ModelLimits | ConstructorParameters<typeof ModelLimits>[0] + 131 : + 132 : /** Normalize model limit input into the canonical `ModelLimits` class. */ + 133 59 : export const make = (input: Input | undefined) => + 134 135 : input instanceof ModelLimits ? input : new ModelLimits(input ?? {}) + 135 : } + 136 : + 137 142 : export class ModelDefaults extends Schema.Class<ModelDefaults>("LLM.ModelDefaults")({ + 138 78 : limits: Schema.optional(ModelLimits), + 139 98 : generation: Schema.optional(GenerationOptions), + 140 104 : providerOptions: Schema.optional(ProviderOptions), + 141 70 : http: Schema.optional(HttpOptions), + 142 10 : }) {} + 143 : + 144 90 : export namespace ModelDefaults { + 145 : export type Input = + 146 : | ModelDefaults + 147 : | { + 148 : readonly limits?: ModelLimits.Input + 149 : readonly generation?: GenerationOptions.Input + 150 : readonly providerOptions?: ProviderOptions + 151 : readonly http?: HttpOptions.Input + 152 : } + 153 : + 154 : /** Normalize selected-model request defaults without applying precedence. */ + 155 0 : export const make = (input: Input) => { + 156 0 : if (input instanceof ModelDefaults) return input + 157 0 : return new ModelDefaults({ + 158 0 : limits: input.limits === undefined ? undefined : ModelLimits.make(input.limits), + 159 0 : generation: input.generation === undefined ? undefined : GenerationOptions.make(input.generation), + 160 0 : providerOptions: input.providerOptions, + 161 0 : http: input.http === undefined ? undefined : HttpOptions.make(input.http), + 162 14 : }) + 163 : } + 164 : } + 165 : + 166 168 : export const ModelToolSchemaCompatibility = Schema.Literals(["gemini", "moonshot"]) + 167 : export type ModelToolSchemaCompatibility = Schema.Schema.Type<typeof ModelToolSchemaCompatibility> + 168 : + 169 162 : export class ModelCompatibility extends Schema.Class<ModelCompatibility>("LLM.ModelCompatibility")({ + 170 116 : toolSchema: Schema.optional(ModelToolSchemaCompatibility), + 171 10 : }) {} + 172 : + 173 110 : export namespace ModelCompatibility { + 174 : export type Input = ModelCompatibility | ConstructorParameters<typeof ModelCompatibility>[0] + 175 : + 176 : /** Normalize model/upstream compatibility metadata without projecting requests. */ + 177 145 : export const make = (input: Input) => (input instanceof ModelCompatibility ? input : new ModelCompatibility(input)) + 178 : } + 179 : + 180 0 : export class Model { + 181 0 : readonly id: ModelID + 182 12 : readonly provider: ProviderID + 183 16 : readonly route: AnyRoute + 184 22 : readonly defaults?: ModelDefaults + 185 31 : readonly compatibility?: ModelCompatibility + 186 : + 187 37 : constructor(input: Model.ConstructorInput) { + 188 46 : this.id = input.id + 189 70 : this.provider = input.provider + 190 58 : this.route = input.route + 191 70 : this.defaults = input.defaults + 192 104 : this.compatibility = input.compatibility + 193 : } + 194 : + 195 30 : static make(input: Model.Input) { + 196 48 : return new Model({ + 197 66 : id: ModelID.make(input.id), + 198 96 : provider: ProviderID.make(input.provider), + 199 50 : route: input.route, + 200 149 : defaults: input.defaults === undefined ? undefined : ModelDefaults.make(input.defaults), + 201 173 : compatibility: input.compatibility === undefined ? undefined : ModelCompatibility.make(input.compatibility), + 202 25 : }) + 203 : } + 204 : + 205 0 : static input(model: Model): Model.ConstructorInput { + 206 0 : return { + 207 0 : id: model.id, + 208 0 : provider: model.provider, + 209 0 : route: model.route, + 210 0 : defaults: model.defaults, + 211 0 : compatibility: model.compatibility, + 212 22 : } + 213 : } + 214 : + 215 0 : static update(model: Model, patch: Partial<Model.Input>) { + 216 0 : if (Object.keys(patch).length === 0) return model + 217 0 : return Model.make({ + 218 0 : ...Model.input(model), + 219 0 : ...patch, + 220 8 : }) + 221 : } + 222 2 : } + 223 : + 224 : export namespace Model { + 225 : export type ConstructorInput = { + 226 : readonly id: ModelID + 227 : readonly provider: ProviderID + 228 : readonly route: AnyRoute + 229 : readonly defaults?: ModelDefaults + 230 : readonly compatibility?: ModelCompatibility + 231 : } + 232 : + 233 : export type Input = Omit<ConstructorInput, "id" | "provider" | "defaults" | "compatibility"> & { + 234 : readonly id: string | ModelID + 235 : readonly provider: string | ProviderID + 236 : readonly defaults?: ModelDefaults.Input + 237 : readonly compatibility?: ModelCompatibility.Input + 238 : } + 239 : } + 240 : + 241 : export type ModelInput = Model.Input + 242 : + 243 206 : export const ModelSchema = Schema.declare((value): value is Model => value instanceof Model, { expected: "LLM.Model" }) + 244 : + 245 126 : export class CacheHint extends Schema.Class<CacheHint>("LLM.CacheHint")({ + 246 106 : type: Schema.Literals(["ephemeral", "persistent"]), + 247 86 : ttlSeconds: Schema.optional(Schema.Number), + 248 10 : }) {} + 249 : + 250 : // Auto-placement policy for prompt caching. The protocol-neutral lowering step + 251 : // reads this and injects `CacheHint`s at the configured boundaries; the + 252 : // per-protocol body builders then translate those hints into wire markers as + 253 : // usual. `"auto"` is the recommended default for agent loops — it places one + 254 : // breakpoint at the last tool definition, one at the last system part, and one + 255 : // at the latest user message. The combination of provider invalidation + 256 : // hierarchy (tools → system → messages) and Anthropic/Bedrock's 20-block + 257 : // lookback means three trailing breakpoints reliably cover the static prefix. + 258 : // + 259 : // Pass `"none"` to opt out entirely (the legacy behavior). Pass the granular + 260 : // object form to override individual choices. + 261 98 : export const CachePolicyObject = Schema.Struct({ + 262 82 : tools: Schema.optional(Schema.Boolean), + 263 84 : system: Schema.optional(Schema.Boolean), + 264 52 : messages: Schema.optional( + 265 36 : Schema.Union([ + 266 84 : Schema.Literal("latest-user-message"), + 267 78 : Schema.Literal("latest-assistant"), + 268 80 : Schema.Struct({ tail: Schema.Number }), + 269 4 : ]), + 270 8 : ), + 271 86 : ttlSeconds: Schema.optional(Schema.Number), + 272 6 : }) + 273 : export type CachePolicyObject = Schema.Schema.Type<typeof CachePolicyObject> + 274 : + 275 217 : export const CachePolicy = Schema.Union([Schema.Literal("auto"), Schema.Literal("none"), CachePolicyObject]) + 276 : export type CachePolicy = Schema.Schema.Type<typeof CachePolicy> ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Effect } from "effect" + 2 81 : import { + 3 : LLMEvent, + 4 : type ToolCallPart, + 5 : ToolFailure, + 6 : ToolOutput, + 7 : ToolResultValue, + 8 : type ToolOutput as ToolOutputType, + 9 : type ToolResultValue as ToolResultValueType, + 10 : } from "./schema" + 11 : import { type AnyTool, type Tools } from "./tool" + 12 : + 13 : export interface ToolSettlement { + 14 : readonly result: ToolResultValueType + 15 : readonly output?: ToolOutputType + 16 : } + 17 : + 18 : export interface DispatchResult extends ToolSettlement { + 19 : readonly events: ReadonlyArray<LLMEvent> + 20 : } + 21 : + 22 : /** Execute one canonical tool call without owning provider IO or continuation. */ + 23 0 : export const dispatch = (tools: Tools, call: ToolCallPart): Effect.Effect<DispatchResult> => { + 24 0 : const tool = tools[call.name] + 25 0 : if (!tool) return Effect.succeed(result(call, { type: "error", value: `Unknown tool: ${call.name}` })) + 26 0 : if (!tool.execute) + 27 0 : return Effect.succeed(result(call, { type: "error", value: `Tool has no execute handler: ${call.name}` })) + 28 0 : + 29 0 : return decodeAndExecute(tool, call).pipe( + 30 0 : Effect.map((value) => result(call, value)), + 31 0 : Effect.catchTag("LLM.ToolFailure", (failure) => + 32 0 : Effect.succeed(result(call, { type: "error", value: failure.message }, failure.error)), + 33 0 : ), + 34 2 : ) + 35 : } + 36 : + 37 0 : const decodeAndExecute = (tool: AnyTool, call: ToolCallPart): Effect.Effect<ToolSettlement, ToolFailure> => + 38 0 : tool._decode(call.input).pipe( + 39 0 : Effect.mapError((error) => new ToolFailure({ message: `Invalid tool input: ${error.message}` })), + 40 0 : Effect.flatMap((decoded) => + 41 0 : tool.execute!(decoded, { id: call.id, name: call.name }).pipe( + 42 0 : Effect.flatMap((value) => + 43 0 : tool._encode(value).pipe( + 44 0 : Effect.mapError( + 45 0 : (error) => + 46 0 : new ToolFailure({ + 47 0 : message: `Tool returned an invalid value for its success schema: ${error.message}`, + 48 0 : }), + 49 0 : ), + 50 0 : ), + 51 0 : ), + 52 0 : Effect.map((encoded) => { + 53 0 : if (tool._legacyResult && ToolResultValue.is(encoded)) + 54 0 : return { result: encoded, output: ToolOutput.fromResultValue(encoded) } + 55 0 : const output = tool._project(decoded, call.id, encoded) + 56 0 : const result = ToolOutput.toResultValue(output) + 57 0 : return result.type === "error" ? { result } : { result, output } + 58 0 : }), + 59 0 : ), + 60 : ), + 61 2 : ) + 62 : + 63 0 : const result = (call: ToolCallPart, value: ToolResultValueType | ToolSettlement, error?: unknown): DispatchResult => { + 64 0 : const settlement = ToolResultValue.is(value) ? { result: value } : value + 65 0 : return { + 66 0 : result: settlement.result, + 67 0 : output: settlement.output, + 68 0 : events: + 69 0 : settlement.result.type === "error" + 70 0 : ? [ + 71 0 : LLMEvent.toolError({ id: call.id, name: call.name, message: String(settlement.result.value), error }), + 72 0 : LLMEvent.toolResult({ id: call.id, name: call.name, result: settlement.result }), + 73 0 : ] + 74 0 : : [LLMEvent.toolResult({ id: call.id, name: call.name, result: settlement.result, output: settlement.output })], + 75 2 : } + 76 : } + 77 : + 78 39 : export const ToolRuntime = { dispatch } as const ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 40 : import { Effect, JsonSchema, Schema } from "effect" + 2 : import type { + 3 : ToolCallPart, + 4 : ToolContent, + 5 : ToolDefinition as ToolDefinitionClass, + 6 : ToolOutput as ToolOutputType, + 7 : } from "./schema" + 8 67 : import { ToolDefinition, ToolFailure, ToolOutput } from "./schema" + 9 : + 10 : /** + 11 : * Schema constraint for tool parameters / success values: no decoding or + 12 : * encoding services are allowed. Tools should be self-contained — anything + 13 : * beyond pure data conversion belongs in the handler closure. + 14 : */ + 15 : export type ToolSchema<T> = Schema.Codec<T, any, never, never> + 16 : export interface ToolExecuteContext { + 17 : readonly id: ToolCallPart["id"] + 18 : readonly name: ToolCallPart["name"] + 19 : } + 20 : + 21 : export type ToolExecute<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = ( + 22 : params: Schema.Schema.Type<Parameters>, + 23 : context?: ToolExecuteContext, + 24 : ) => Effect.Effect<Schema.Schema.Type<Success>, ToolFailure> + 25 : + 26 : export interface ToolModelOutputInput<Parameters, Output> { + 27 : readonly callID: ToolCallPart["id"] + 28 : readonly parameters: Parameters + 29 : readonly output: Output + 30 : } + 31 : + 32 : export type ToolToModelOutput<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = ( + 33 : input: ToolModelOutputInput<Schema.Schema.Type<Parameters>, Success["Encoded"]>, + 34 : ) => ReadonlyArray<ToolContent> + 35 : + 36 : /** + 37 : * A type-safe LLM tool. Each tool bundles its own description, parameter + 38 : * Schema and success Schema. The execute handler is optional: omit it when you + 39 : * only want to expose a tool schema to the model and handle tool calls outside + 40 : * this package. + 41 : * + 42 : * Errors must be expressed as `ToolFailure`. Unmapped errors and defects fail + 43 : * the stream. + 44 : * + 45 : * Internally each tool also carries memoized codecs and a precomputed + 46 : * `ToolDefinition` so callers do not rebuild them per invocation. + 47 : */ + 48 : export interface Tool<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> { + 49 : readonly description: string + 50 : readonly parameters: Parameters + 51 : readonly success: Success + 52 : readonly execute?: ToolExecute<Parameters, Success> + 53 : readonly toModelOutput?: ToolToModelOutput<Parameters, Success> + 54 : readonly toStructuredOutput?: (output: Success["Encoded"]) => unknown + 55 : /** @internal */ + 56 : readonly _decode: (input: unknown) => Effect.Effect<Schema.Schema.Type<Parameters>, Schema.SchemaError> + 57 : /** @internal */ + 58 : readonly _encode: (value: Schema.Schema.Type<Success>) => Effect.Effect<unknown, Schema.SchemaError> + 59 : /** @internal */ + 60 : readonly _project: ( + 61 : parameters: Schema.Schema.Type<Parameters>, + 62 : callID: ToolCallPart["id"], + 63 : output: unknown, + 64 : ) => ToolOutputType + 65 : /** @internal */ + 66 : readonly _legacyResult: boolean + 67 : /** @internal */ + 68 : readonly _definition: ToolDefinitionClass + 69 : } + 70 : + 71 : export type AnyTool = Tool<any, any> + 72 : + 73 : export type ExecutableTool<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = Tool< + 74 : Parameters, + 75 : Success + 76 : > & { + 77 : readonly execute: ToolExecute<Parameters, Success> + 78 : } + 79 : + 80 : export type AnyExecutableTool = ExecutableTool<any, any> + 81 : + 82 : export type ExecutableTools = Record<string, AnyExecutableTool> + 83 : + 84 : type TypedToolConfig = { + 85 : readonly description: string + 86 : readonly parameters: ToolSchema<any> + 87 : readonly success: ToolSchema<any> + 88 : readonly execute?: ToolExecute<ToolSchema<any>, ToolSchema<any>> + 89 : readonly toModelOutput?: ToolToModelOutput<ToolSchema<any>, ToolSchema<any>> + 90 : readonly toStructuredOutput?: (output: unknown) => unknown + 91 : } + 92 : + 93 : type DynamicToolConfig = { + 94 : readonly description: string + 95 : readonly jsonSchema: JsonSchema.JsonSchema + 96 : readonly outputSchema?: JsonSchema.JsonSchema + 97 : readonly execute?: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure> + 98 : readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent> + 99 : readonly toStructuredOutput?: (output: unknown) => unknown + 100 : } + 101 : + 102 : /** + 103 : * Constructs a tool. Two input modes: + 104 : * + 105 : * 1. **Typed** — pass Effect `parameters` and `success` Schemas; inputs and + 106 : * outputs are statically typed and decoded/encoded automatically. + 107 : * + 108 : * ```ts + 109 : * Tool.make({ + 110 : * description: "Get current weather", + 111 : * parameters: Schema.Struct({ city: Schema.String }), + 112 : * success: Schema.Struct({ temperature: Schema.Number }), + 113 : * execute: ({ city }) => Effect.succeed({ temperature: 22 }), + 114 : * }) + 115 : * ``` + 116 : * + 117 : * 2. **Dynamic** — pass raw JSON Schema as `jsonSchema`. Use this when the + 118 : * schema comes from an external source (MCP server, plugin manifest, + 119 : * dynamic config) and is not known at compile time. Inputs are typed as + 120 : * `unknown`; the handler is responsible for any validation it needs. + 121 : * + 122 : * ```ts + 123 : * Tool.make({ + 124 : * description: "Look something up", + 125 : * jsonSchema: { type: "object", properties: { ... } }, + 126 : * execute: (params) => Effect.succeed(...), + 127 : * }) + 128 : * ``` + 129 : * + 130 : * In both modes the produced tool flows through `toDefinitions(...)` + 131 : * identically. + 132 : */ + 133 : export function make<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>>(config: { + 134 : readonly description: string + 135 : readonly parameters: Parameters + 136 : readonly success: Success + 137 : readonly execute: ToolExecute<Parameters, Success> + 138 : readonly toModelOutput?: ToolToModelOutput<Parameters, Success> + 139 : readonly toStructuredOutput?: (output: Success["Encoded"]) => unknown + 140 : }): ExecutableTool<Parameters, Success> + 141 : export function make<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>>(config: { + 142 : readonly description: string + 143 : readonly parameters: Parameters + 144 : readonly success: Success + 145 : readonly execute?: undefined + 146 : readonly toModelOutput?: ToolToModelOutput<Parameters, Success> + 147 : readonly toStructuredOutput?: (output: Success["Encoded"]) => unknown + 148 : }): Tool<Parameters, Success> + 149 : export function make(config: { + 150 : readonly description: string + 151 : readonly jsonSchema: JsonSchema.JsonSchema + 152 : readonly outputSchema?: JsonSchema.JsonSchema + 153 : readonly execute: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure> + 154 : readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent> + 155 : readonly toStructuredOutput?: (output: unknown) => unknown + 156 : }): AnyExecutableTool + 157 : export function make(config: { + 158 : readonly description: string + 159 : readonly jsonSchema: JsonSchema.JsonSchema + 160 : readonly outputSchema?: JsonSchema.JsonSchema + 161 : readonly execute?: undefined + 162 : readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent> + 163 : readonly toStructuredOutput?: (output: unknown) => unknown + 164 : }): AnyTool + 165 0 : export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool { + 166 0 : if ("jsonSchema" in config) { + 167 0 : return { + 168 0 : description: config.description, + 169 0 : parameters: Schema.Unknown as ToolSchema<unknown>, + 170 0 : success: Schema.Unknown as ToolSchema<unknown>, + 171 0 : execute: config.execute, + 172 0 : toModelOutput: config.toModelOutput, + 173 0 : toStructuredOutput: config.toStructuredOutput, + 174 0 : _decode: Effect.succeed, + 175 0 : _encode: Effect.succeed, + 176 0 : _project: (parameters, callID, output) => + 177 0 : project(config.toModelOutput, config.toStructuredOutput, parameters, callID, output), + 178 0 : _legacyResult: config.toModelOutput === undefined && config.toStructuredOutput === undefined, + 179 0 : _definition: new ToolDefinition({ + 180 0 : name: "", + 181 0 : description: config.description, + 182 0 : inputSchema: config.jsonSchema, + 183 0 : outputSchema: config.outputSchema, + 184 0 : }), + 185 0 : } + 186 0 : } + 187 0 : return { + 188 0 : description: config.description, + 189 0 : parameters: config.parameters, + 190 0 : success: config.success, + 191 0 : execute: config.execute, + 192 0 : toModelOutput: config.toModelOutput, + 193 0 : toStructuredOutput: config.toStructuredOutput, + 194 0 : _decode: Schema.decodeUnknownEffect(config.parameters), + 195 0 : _encode: Schema.encodeEffect(config.success), + 196 0 : _project: (parameters, callID, output) => + 197 0 : project(config.toModelOutput, config.toStructuredOutput, parameters, callID, output), + 198 0 : _legacyResult: false, + 199 0 : _definition: new ToolDefinition({ + 200 0 : name: "", + 201 0 : description: config.description, + 202 0 : inputSchema: toJsonSchema(config.parameters), + 203 0 : outputSchema: toJsonSchema(config.success), + 204 0 : }), + 205 1 : } + 206 : } + 207 : + 208 : /** + 209 : * A record of named tools. The record key becomes the tool name on the wire. + 210 : */ + 211 : export type Tools = Record<string, AnyTool> + 212 : + 213 : /** + 214 : * Convert a tools record into the `ToolDefinition[]` shape that + 215 : * `LLMRequest.tools` expects. + 216 : * + 217 : * Tool names come from the record keys, so the per-tool cached + 218 : * `_definition` is rebuilt with the correct name here. The JSON Schema body + 219 : * is reused. + 220 : */ + 221 0 : export const toDefinitions = (tools: Tools): ReadonlyArray<ToolDefinitionClass> => + 222 0 : Object.entries(tools).map( + 223 0 : ([name, item]) => + 224 0 : new ToolDefinition({ + 225 0 : name, + 226 0 : description: item._definition.description, + 227 0 : inputSchema: item._definition.inputSchema, + 228 0 : outputSchema: item._definition.outputSchema, + 229 : }), + 230 2 : ) + 231 : + 232 0 : const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => { + 233 0 : const document = Schema.toJsonSchemaDocument(schema) + 234 0 : if (Object.keys(document.definitions).length === 0) return document.schema + 235 2 : return { ...document.schema, $defs: document.definitions } + 236 : } + 237 : + 238 0 : const project = ( + 239 0 : toModelOutput: ((input: ToolModelOutputInput<any, any>) => ReadonlyArray<ToolContent>) | undefined, + 240 0 : toStructuredOutput: ((output: unknown) => unknown) | undefined, + 241 0 : parameters: unknown, + 242 0 : callID: ToolCallPart["id"], + 243 0 : output: unknown, + 244 0 : ): ToolOutputType => + 245 0 : ToolOutput.make( + 246 0 : toStructuredOutput?.(output) ?? output, + 247 0 : toModelOutput?.({ callID, parameters, output }) ?? + 248 : (typeof output === "string" ? [{ type: "text", text: output }] : []), + 249 2 : ) + 250 : + 251 23 : export { ToolFailure } + 252 : + 253 30 : export * as Tool from "./tool" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| record.ts | +
+ |
+ 100.0 % | +2 | +2 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : /** Plain-record narrowing. Excludes arrays so JSON object checks don't accept tuples as key/value bags. */ + 2 33 : export const isRecord = (value: unknown): value is Record<string, unknown> => + 3 68 : typeof value === "object" && value !== null && !Array.isArray(value) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| index.ts | +
+ |
+ 100.0 % | +1 | +1 | +|
| plugin.ts | +
+ |
+ 100.0 % | +2 | +2 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| index.ts | +
+ |
+ 100.0 % | +1 | +1 | +|
| plugin.ts | +
+ |
+ 100.0 % | +2 | +2 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| index.ts | +
+ |
+ 100.0 % | +1 | +1 | +|
| plugin.ts | +
+ |
+ 100.0 % | +2 | +2 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : export type { PluginContext } from "./context.js" + 2 36 : export { define } from "./plugin.js" + 3 : export type { Plugin } from "./plugin.js" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : import type { Effect, Scope } from "effect" + 2 : import type { PluginContext } from "./context.js" + 3 : + 4 : export interface Plugin<R = Scope.Scope> { + 5 : readonly id: string + 6 : readonly effect: (context: PluginContext) => Effect.Effect<void, never, R> + 7 : } + 8 : + 9 17 : export function define<R = Scope.Scope>(plugin: Plugin<R>) { + 10 13 : return plugin + 11 : } + 12 : + 13 : export interface PluginDomain { + 14 : readonly add: (plugin: Plugin) => Effect.Effect<void> + 15 : readonly remove: (id: string) => Effect.Effect<void> + 16 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| index.ts | +
+ |
+ 100.0 % | +1 | +1 | +|
| plugin.ts | +
+ |
+ 100.0 % | +2 | +2 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| index.ts | +
+ |
+ 100.0 % | +1 | +1 | +|
| plugin.ts | +
+ |
+ 100.0 % | +2 | +2 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| index.ts | +
+ |
+ 100.0 % | +1 | +1 | +|
| plugin.ts | +
+ |
+ 100.0 % | +2 | +2 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : export type { PluginContext } from "./context.js" + 2 : export type { PluginOptions } from "../options.js" + 3 36 : export { define } from "./plugin.js" + 4 : export type { Plugin, PluginDomain } from "./plugin.js" + 5 : export type { Registration, Reload } from "./registration.js" + 6 : export type { AgentDraft, AgentHooks } from "./agent.js" + 7 : export type { AISDKHooks } from "./aisdk.js" + 8 : export type { CatalogDraft, CatalogHooks, CatalogProviderRecord } from "./catalog.js" + 9 : export type { CommandDraft, CommandHooks } from "./command.js" + 10 : export type { IntegrationDraft, IntegrationHooks, IntegrationMethodRegistration } from "./integration.js" + 11 : export type { ReferenceDraft, ReferenceHooks } from "./reference.js" + 12 : export type { SkillDraft, SkillHooks } from "./skill.js" ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 : import type { PluginContext } from "./context.js" + 2 : + 3 : export interface Plugin { + 4 : readonly id: string + 5 : readonly setup: (context: PluginContext) => Promise<void> | void + 6 : } + 7 : + 8 17 : export function define(plugin: Plugin) { + 9 13 : return plugin + 10 : } + 11 : + 12 : export interface PluginDomain { + 13 : readonly add: (plugin: Plugin) => Promise<void> + 14 : readonly remove: (id: string) => Promise<void> + 15 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 33 : export * as Agent from "./agent" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 32 : import { Model } from "./model" + 6 42 : import { Permission } from "./permission" + 7 38 : import { Provider } from "./provider" + 8 48 : import { PositiveInt, statics } from "./schema" + 9 : + 10 65 : export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID")) + 11 : export type ID = typeof ID.Type + 12 : + 13 36 : export const Color = Schema.Union([ + 14 61 : Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), + 15 91 : Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), + 16 43 : ]).annotate({ identifier: "Agent.Color" }) + 17 : export type Color = typeof Color.Type + 18 : + 19 : export interface Info extends Schema.Schema.Type<typeof Info> {} + 20 36 : export const Info = Schema.Struct({ + 21 9 : id: ID, + 22 34 : model: Model.Ref.pipe(optional), + 23 28 : request: Provider.Request, + 24 39 : system: Schema.String.pipe(optional), + 25 44 : description: Schema.String.pipe(optional), + 26 56 : mode: Schema.Literals(["subagent", "primary", "all"]), + 27 25 : hidden: Schema.Boolean, + 28 30 : color: Color.pipe(optional), + 29 36 : steps: PositiveInt.pipe(optional), + 30 32 : permissions: Permission.Ruleset, + 31 2 : }) + 32 41 : .annotate({ identifier: "AgentV2.Info" }) + 33 5 : .pipe( + 34 23 : statics((schema) => ({ + 35 14 : empty: (id: ID) => + 36 100 : schema.make({ id, request: { headers: {}, body: {} }, mode: "all", hidden: false, permissions: [] }), + 37 1 : })), + 38 2 : ) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 37 : export * as Catalog from "./catalog" + 2 : + 3 44 : import { define, inventory } from "./event" + 4 : + 5 64 : const Updated = define({ type: "catalog.updated", schema: {} }) + 6 65 : export const Event = { Updated, Definitions: inventory(Updated) } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 37 : export * as Command from "./command" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 32 : import { Model } from "./model" + 6 : + 7 : export interface Info extends Schema.Schema.Type<typeof Info> {} + 8 36 : export const Info = Schema.Struct({ + 9 22 : name: Schema.String, + 10 26 : template: Schema.String, + 11 44 : description: Schema.String.pipe(optional), + 12 38 : agent: Schema.String.pipe(optional), + 13 34 : model: Model.Ref.pipe(optional), + 14 39 : subtask: Schema.Boolean.pipe(optional), + 15 45 : }).annotate({ identifier: "CommandV2.Info" }) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 43 : export * as Connection from "./connection" + 2 : + 3 32 : import { Schema } from "effect" + 4 42 : import { Credential } from "./credential" + 5 : + 6 : export interface CredentialInfo extends Schema.Schema.Type<typeof CredentialInfo> {} + 7 46 : export const CredentialInfo = Schema.Struct({ + 8 37 : type: Schema.Literal("credential"), + 9 20 : id: Credential.ID, + 10 21 : label: Schema.String, + 11 57 : }).annotate({ identifier: "Connection.CredentialInfo" }) + 12 : + 13 : export interface EnvInfo extends Schema.Schema.Type<typeof EnvInfo> {} + 14 39 : export const EnvInfo = Schema.Struct({ + 15 30 : type: Schema.Literal("env"), + 16 20 : name: Schema.String, + 17 50 : }).annotate({ identifier: "Connection.EnvInfo" }) + 18 : + 19 59 : export const Info = Schema.Union([CredentialInfo, EnvInfo]) + 20 35 : .pipe(Schema.toTaggedUnion("type")) + 21 44 : .annotate({ identifier: "Connection.Info" }) + 22 : export type Info = typeof Info.Type ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 43 : export * as Credential from "./credential" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 55 : import { IntegrationMethodID } from "./integration-id" + 6 41 : import { ascending } from "./identifier" + 7 51 : import { NonNegativeInt, statics } from "./schema" + 8 : + 9 36 : export const ID = Schema.String.pipe( + 10 31 : Schema.brand("Credential.ID"), + 11 71 : statics((schema) => ({ create: () => schema.make("cred_" + ascending()) })), + 12 3 : ) + 13 : export type ID = typeof ID.Type + 14 : + 15 : export interface OAuth extends Schema.Schema.Type<typeof OAuth> {} + 16 37 : export const OAuth = Schema.Struct({ + 17 32 : type: Schema.Literal("oauth"), + 18 32 : methodID: IntegrationMethodID, + 19 25 : refresh: Schema.String, + 20 24 : access: Schema.String, + 21 26 : expires: NonNegativeInt, + 22 65 : metadata: optional(Schema.Record(Schema.String, Schema.Unknown)), + 23 48 : }).annotate({ identifier: "Credential.OAuth" }) + 24 : + 25 : export interface Key extends Schema.Schema.Type<typeof Key> {} + 26 35 : export const Key = Schema.Struct({ + 27 30 : type: Schema.Literal("key"), + 28 21 : key: Schema.String, + 29 65 : metadata: optional(Schema.Record(Schema.String, Schema.Unknown)), + 30 46 : }).annotate({ identifier: "Credential.Key" }) + 31 : + 32 47 : export const Value = Schema.Union([OAuth, Key]) + 33 35 : .pipe(Schema.toTaggedUnion("type")) + 34 45 : .annotate({ identifier: "Credential.Value" }) + 35 : export type Value = Schema.Schema.Type<typeof Value> ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 65 : export * as DurableEventManifest from "./durable-event-manifest" + 2 : + 3 32 : import { Event } from "./event" + 4 47 : import { SessionEvent } from "./session-event" + 5 41 : import { SessionV1 } from "./session-v1" + 6 : + 7 32 : export const SessionDurable = { + 8 62 : definitions: Event.durable(SessionEvent.DurableDefinitions), + 9 29 : schema: SessionEvent.Durable, + 10 2 : } as const + 11 : + 12 39 : export const Durable = Event.durable([ + 13 88 : ...SessionV1.Event.Definitions.filter((definition) => definition.durable !== undefined), + 14 35 : ...SessionEvent.DurableDefinitions, + 15 2 : ]) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 33 : export * as Event from "./event" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 41 : import { ascending } from "./identifier" + 6 38 : import { Location } from "./location" + 7 35 : import { statics } from "./schema" + 8 : + 9 71 : export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe( + 10 26 : Schema.brand("Event.ID"), + 11 70 : statics((schema) => ({ create: () => schema.make("evt_" + ascending()) })), + 12 3 : ) + 13 : export type ID = typeof ID.Type + 14 : + 15 : export type Definition< + 16 : Type extends string = string, + 17 : DataSchema extends Schema.Codec<unknown, unknown> = Schema.Codec<unknown, unknown>, + 18 : > = Schema.Top & { + 19 : readonly type: Type + 20 : readonly durable?: { + 21 : readonly version: number + 22 : readonly aggregate: string + 23 : } + 24 : readonly data: DataSchema + 25 : } + 26 : + 27 : export type Data<D extends Definition> = Schema.Schema.Type<D["data"]> + 28 : + 29 : export type Payload<D extends Definition = Definition> = { + 30 : readonly id: ID + 31 : readonly type: D["type"] + 32 : readonly data: Data<D> + 33 : readonly durable?: { + 34 : readonly aggregateID: string + 35 : readonly seq: number + 36 : readonly version: number + 37 : } + 38 : readonly location?: Location.Ref + 39 : readonly metadata?: Record<string, unknown> + 40 : } + 41 : + 42 5 : export function define< + 43 : const Type extends string, + 44 : const Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>, + 45 8 : >(input: { + 46 : readonly type: Type + 47 : readonly durable?: { + 48 : readonly version: number + 49 : readonly aggregate: string + 50 : } + 51 : readonly schema: Fields + 52 3 : }) { + 53 43 : const data = Schema.Struct(input.schema) + 54 26 : return Schema.Struct({ + 55 11 : id: ID, + 56 69 : metadata: optional(Schema.Record(Schema.String, Schema.Unknown)), + 57 37 : type: Schema.Literal(input.type), + 58 107 : durable: optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Int, version: Schema.Int })), + 59 37 : location: optional(Location.Ref), + 60 6 : data, + 61 3 : }) + 62 37 : .annotate({ identifier: input.type }) + 63 5 : .pipe( + 64 19 : statics(() => ({ + 65 24 : type: input.type, + 66 63 : ...(input.durable === undefined ? {} : { durable: input.durable }), + 67 6 : data, + 68 2 : })), + 69 2 : ) satisfies Definition<Type, typeof data> + 70 : } + 71 : + 72 25 : export function inventory<const Definitions extends ReadonlyArray<Definition>>(...definitions: Definitions) { + 73 34 : return Object.freeze(definitions) + 74 : } + 75 : + 76 22 : export function latest(definitions: ReadonlyArray<Definition>) { + 77 19 : return readonlyMap( + 78 47 : definitions.reduce((result, definition) => { + 79 49 : const existing = result.get(definition.type) + 80 21 : if (!existing) { + 81 46 : result.set(definition.type, definition) + 82 13 : return result + 83 4 : } + 84 109 : if (definition.durable && existing.durable && definition.durable.version !== existing.durable.version) { + 85 110 : if (definition.durable.version > existing.durable.version) result.set(definition.type, definition) + 86 13 : return result + 87 0 : } + 88 0 : if (definition !== existing) throw new Error(`Duplicate latest event definition for ${definition.type}`) + 89 2 : return result + 90 8 : }, new Map<string, Definition>()), + 91 2 : ) + 92 : } + 93 : + 94 24 : export function versionedType(type: string, version: number) { + 95 28 : return `${type}.${version}` + 96 : } + 97 : + 98 22 : export function durable<const Definitions extends ReadonlyArray<Definition>>(definitions: Definitions) { + 99 19 : return readonlyMap( + 100 47 : definitions.reduce((result, definition) => { + 101 33 : if (!definition.durable) return result + 102 75 : const key = versionedType(definition.type, definition.durable.version) + 103 29 : if (result.has(key)) throw new Error(`Duplicate durable event definition for ${key}`) + 104 32 : result.set(key, definition) + 105 15 : return result + 106 8 : }, new Map<string, Definitions[number]>()), + 107 1 : ) + 108 : } + 109 : + 110 9 : function readonlyMap<Key, Value>(map: Map<Key, Value>): ReadonlyMap<Key, Value> { + 111 0 : const result: ReadonlyMap<Key, Value> = Object.freeze({ + 112 0 : get size() { + 113 5 : return map.size + 114 : }, + 115 13 : entries: () => map.entries(), + 116 0 : forEach: (callback: (value: Value, key: Key, map: ReadonlyMap<Key, Value>) => void, thisArg?: unknown) => + 117 5 : map.forEach((value, key) => callback.call(thisArg, value, key, result)), + 118 29 : get: (key: Key) => map.get(key), + 119 9 : has: (key: Key) => map.has(key), + 120 25 : keys: () => map.keys(), + 121 13 : values: () => map.values(), + 122 19 : [Symbol.iterator]: () => map[Symbol.iterator](), + 123 5 : }) + 124 13 : return result + 125 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 40 : export * as FileDiff from "./file-diff" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 : + 6 36 : export const Info = Schema.Struct({ + 7 32 : file: optional(Schema.String), + 8 33 : patch: optional(Schema.String), + 9 27 : additions: Schema.Finite, + 10 27 : deletions: Schema.Finite, + 11 68 : status: optional(Schema.Literals(["added", "deleted", "modified"])), + 12 47 : }).annotate({ identifier: "SnapshotFileDiff" }) + 13 : export interface Info extends Schema.Schema.Type<typeof Info> {} ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 58 : export * as FileSystemWatcher from "./filesystem-watcher" + 2 : + 3 32 : import { Schema } from "effect" + 4 44 : import { define, inventory } from "./event" + 5 : + 6 25 : const Updated = define({ + 7 31 : type: "file.watcher.updated", + 8 13 : schema: { + 9 24 : file: Schema.String, + 10 53 : event: Schema.Literals(["add", "change", "unlink"]), + 11 2 : }, + 12 3 : }) + 13 65 : export const Event = { Updated, Definitions: inventory(Updated) } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 5 : export * as FileSystem from "./filesystem" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 44 : import { define, inventory } from "./event" + 6 69 : import { NonNegativeInt, PositiveInt, RelativePath } from "./schema" + 7 : + 8 24 : const Edited = define({ + 9 22 : type: "file.edited", + 10 32 : schema: { file: Schema.String }, + 11 3 : }) + 12 64 : export const Event = { Edited, Definitions: inventory(Edited) } + 13 : + 14 : export interface Entry extends Schema.Schema.Type<typeof Entry> {} + 15 37 : export const Entry = Schema.Struct({ + 16 21 : path: RelativePath, + 17 45 : type: Schema.Literals(["file", "directory"]), + 18 48 : }).annotate({ identifier: "FileSystem.Entry" }) + 19 : + 20 : export interface Submatch extends Schema.Schema.Type<typeof Submatch> {} + 21 40 : export const Submatch = Schema.Struct({ + 22 22 : text: Schema.String, + 23 24 : start: NonNegativeInt, + 24 20 : end: NonNegativeInt, + 25 51 : }).annotate({ identifier: "FileSystem.Submatch" }) + 26 : + 27 : export interface Match extends Schema.Schema.Type<typeof Match> {} + 28 37 : export const Match = Schema.Struct({ + 29 15 : entry: Entry, + 30 20 : line: PositiveInt, + 31 25 : offset: NonNegativeInt, + 32 22 : text: Schema.String, + 33 35 : submatches: Schema.Array(Submatch), + 34 48 : }).annotate({ identifier: "FileSystem.Match" }) + 35 : + 36 70 : export class FindInput extends Schema.Class<FindInput>("FileSystem.FindInput")({ + 37 23 : query: Schema.String, + 38 62 : type: Schema.Literals(["file", "directory"]).pipe(optional), + 39 34 : limit: PositiveInt.pipe(optional), + 40 4 : }) {} ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 18 : const length = 26 + 2 144 : const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + 3 22 : let lastTimestamp = 0 + 4 16 : let counter = 0 + 5 : + 6 11 : export function ascending() { + 7 21 : return create(false) + 8 : } + 9 : + 10 11 : export function descending() { + 11 20 : return create(true) + 12 : } + 13 : + 14 45 : export function create(descending: boolean, timestamp = Date.now()) { + 15 37 : if (timestamp !== lastTimestamp) { + 16 30 : lastTimestamp = timestamp + 17 14 : counter = 0 + 18 2 : } + 19 12 : counter++ + 20 : + 21 64 : const current = BigInt(timestamp) * 0x1000n + BigInt(counter) + 22 45 : const value = descending ? ~current : current + 23 52 : const time = Array.from({ length: 6 }, (_, index) => + 24 48 : Number((value >> BigInt(40 - 8 * index)) & 0xffn) + 25 13 : .toString(16) + 26 15 : .padStart(2, "0"), + 27 13 : ).join("") + 28 59 : const bytes = crypto.getRandomValues(new Uint8Array(length - 12)) + 29 60 : return time + Array.from(bytes, (byte) => chars[byte % 62]).join("") + 30 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| agent.ts | +
+ |
+ 100.0 % | +31 | +31 | +|
| catalog.ts | +
+ |
+ 100.0 % | +4 | +4 | +|
| command.ts | +
+ |
+ 100.0 % | +12 | +12 | +|
| connection.ts | +
+ |
+ 100.0 % | +15 | +15 | +|
| credential.ts | +
+ |
+ 100.0 % | +26 | +26 | +|
| durable-event-manifest.ts | +
+ |
+ 100.0 % | +12 | +12 | +|
| event.ts | +
+ |
+ 93.2 % | +74 | +69 | +|
| file-diff.ts | +
+ |
+ 100.0 % | +10 | +10 | +|
| filesystem-watcher.ts | +
+ |
+ 100.0 % | +11 | +11 | +|
| filesystem.ts | +
+ |
+ 100.0 % | +31 | +31 | +|
| identifier.ts | +
+ |
+ 100.0 % | +23 | +23 | +|
| integration-id.ts | +
+ |
+ 100.0 % | +3 | +3 | +|
| integration.ts | +
+ |
+ 100.0 % | +96 | +96 | +|
| llm.ts | +
+ |
+ 100.0 % | +19 | +19 | +|
| location.ts | +
+ |
+ 94.4 % | +18 | +17 | +|
| model.ts | +
+ |
+ 100.0 % | +89 | +89 | +|
| models-dev.ts | +
+ |
+ 100.0 % | +7 | +7 | +|
| permission-saved.ts | +
+ |
+ 100.0 % | +15 | +15 | +|
| permission-v1.ts | +
+ |
+ 100.0 % | +1 | +1 | +|
| permission.ts | +
+ |
+ 100.0 % | +48 | +48 | +|
| plugin.ts | +
+ |
+ 100.0 % | +9 | +9 | +|
| project-copy.ts | +
+ |
+ 100.0 % | +21 | +21 | +|
| project-directories.ts | +
+ |
+ 100.0 % | +8 | +8 | +|
| project-id.ts | +
+ |
+ 100.0 % | +6 | +6 | +|
| project.ts | +
+ |
+ 100.0 % | +34 | +34 | +|
| prompt.ts | +
+ |
+ 85.7 % | +49 | +42 | +|
| provider.ts | +
+ |
+ 100.0 % | +59 | +59 | +|
| pty-ticket.ts | +
+ |
+ 100.0 % | +7 | +7 | +|
| pty.ts | +
+ |
+ 100.0 % | +47 | +47 | +|
| question.ts | +
+ |
+ 100.0 % | +67 | +67 | +|
| reference.ts | +
+ |
+ 100.0 % | +30 | +30 | +|
| revert.ts | +
+ |
+ 100.0 % | +19 | +19 | +|
| schema.ts | +
+ |
+ 100.0 % | +22 | +22 | +|
| session-delivery.ts | +
+ |
+ 100.0 % | +3 | +3 | +|
| session-event.ts | +
+ |
+ 100.0 % | +429 | +429 | +|
| session-id.ts | +
+ |
+ 100.0 % | +13 | +13 | +|
| session-input.ts | +
+ |
+ 100.0 % | +18 | +18 | +|
| session-message.ts | +
+ |
+ 100.0 % | +170 | +170 | +|
| session-todo.ts | +
+ |
+ 100.0 % | +21 | +21 | +|
| session-v1.ts | +
+ |
+ 100.0 % | +1 | +1 | +|
| session.ts | +
+ |
+ 100.0 % | +43 | +43 | +|
| skill.ts | +
+ |
+ 95.2 % | +42 | +40 | +|
| workspace-event.ts | +
+ |
+ 100.0 % | +25 | +25 | +|
| workspace-id.ts | +
+ |
+ 93.8 % | +16 | +15 | +|
| workspace.ts | +
+ |
+ 100.0 % | +5 | +5 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| prompt.ts | +
+ |
+ 85.7 % | +49 | +42 | +|
| event.ts | +
+ |
+ 93.2 % | +74 | +69 | +|
| workspace-id.ts | +
+ |
+ 93.8 % | +16 | +15 | +|
| location.ts | +
+ |
+ 94.4 % | +18 | +17 | +|
| skill.ts | +
+ |
+ 95.2 % | +42 | +40 | +|
| permission-v1.ts | +
+ |
+ 100.0 % | +1 | +1 | +|
| session-v1.ts | +
+ |
+ 100.0 % | +1 | +1 | +|
| integration-id.ts | +
+ |
+ 100.0 % | +3 | +3 | +|
| session-delivery.ts | +
+ |
+ 100.0 % | +3 | +3 | +|
| catalog.ts | +
+ |
+ 100.0 % | +4 | +4 | +|
| workspace.ts | +
+ |
+ 100.0 % | +5 | +5 | +|
| project-id.ts | +
+ |
+ 100.0 % | +6 | +6 | +|
| models-dev.ts | +
+ |
+ 100.0 % | +7 | +7 | +|
| pty-ticket.ts | +
+ |
+ 100.0 % | +7 | +7 | +|
| project-directories.ts | +
+ |
+ 100.0 % | +8 | +8 | +|
| plugin.ts | +
+ |
+ 100.0 % | +9 | +9 | +|
| file-diff.ts | +
+ |
+ 100.0 % | +10 | +10 | +|
| filesystem-watcher.ts | +
+ |
+ 100.0 % | +11 | +11 | +|
| command.ts | +
+ |
+ 100.0 % | +12 | +12 | +|
| durable-event-manifest.ts | +
+ |
+ 100.0 % | +12 | +12 | +|
| session-id.ts | +
+ |
+ 100.0 % | +13 | +13 | +|
| connection.ts | +
+ |
+ 100.0 % | +15 | +15 | +|
| permission-saved.ts | +
+ |
+ 100.0 % | +15 | +15 | +|
| session-input.ts | +
+ |
+ 100.0 % | +18 | +18 | +|
| llm.ts | +
+ |
+ 100.0 % | +19 | +19 | +|
| revert.ts | +
+ |
+ 100.0 % | +19 | +19 | +|
| project-copy.ts | +
+ |
+ 100.0 % | +21 | +21 | +|
| session-todo.ts | +
+ |
+ 100.0 % | +21 | +21 | +|
| schema.ts | +
+ |
+ 100.0 % | +22 | +22 | +|
| identifier.ts | +
+ |
+ 100.0 % | +23 | +23 | +|
| workspace-event.ts | +
+ |
+ 100.0 % | +25 | +25 | +|
| credential.ts | +
+ |
+ 100.0 % | +26 | +26 | +|
| reference.ts | +
+ |
+ 100.0 % | +30 | +30 | +|
| agent.ts | +
+ |
+ 100.0 % | +31 | +31 | +|
| filesystem.ts | +
+ |
+ 100.0 % | +31 | +31 | +|
| project.ts | +
+ |
+ 100.0 % | +34 | +34 | +|
| session.ts | +
+ |
+ 100.0 % | +43 | +43 | +|
| pty.ts | +
+ |
+ 100.0 % | +47 | +47 | +|
| permission.ts | +
+ |
+ 100.0 % | +48 | +48 | +|
| provider.ts | +
+ |
+ 100.0 % | +59 | +59 | +|
| question.ts | +
+ |
+ 100.0 % | +67 | +67 | +|
| model.ts | +
+ |
+ 100.0 % | +89 | +89 | +|
| integration.ts | +
+ |
+ 100.0 % | +96 | +96 | +|
| session-message.ts | +
+ |
+ 100.0 % | +170 | +170 | +|
| session-event.ts | +
+ |
+ 100.0 % | +429 | +429 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| agent.ts | +
+ |
+ 100.0 % | +31 | +31 | +|
| catalog.ts | +
+ |
+ 100.0 % | +4 | +4 | +|
| command.ts | +
+ |
+ 100.0 % | +12 | +12 | +|
| connection.ts | +
+ |
+ 100.0 % | +15 | +15 | +|
| credential.ts | +
+ |
+ 100.0 % | +26 | +26 | +|
| durable-event-manifest.ts | +
+ |
+ 100.0 % | +12 | +12 | +|
| event.ts | +
+ |
+ 93.2 % | +74 | +69 | +|
| file-diff.ts | +
+ |
+ 100.0 % | +10 | +10 | +|
| filesystem-watcher.ts | +
+ |
+ 100.0 % | +11 | +11 | +|
| filesystem.ts | +
+ |
+ 100.0 % | +31 | +31 | +|
| identifier.ts | +
+ |
+ 100.0 % | +23 | +23 | +|
| integration-id.ts | +
+ |
+ 100.0 % | +3 | +3 | +|
| integration.ts | +
+ |
+ 100.0 % | +96 | +96 | +|
| llm.ts | +
+ |
+ 100.0 % | +19 | +19 | +|
| location.ts | +
+ |
+ 94.4 % | +18 | +17 | +|
| model.ts | +
+ |
+ 100.0 % | +89 | +89 | +|
| models-dev.ts | +
+ |
+ 100.0 % | +7 | +7 | +|
| permission-saved.ts | +
+ |
+ 100.0 % | +15 | +15 | +|
| permission-v1.ts | +
+ |
+ 100.0 % | +1 | +1 | +|
| permission.ts | +
+ |
+ 100.0 % | +48 | +48 | +|
| plugin.ts | +
+ |
+ 100.0 % | +9 | +9 | +|
| project-copy.ts | +
+ |
+ 100.0 % | +21 | +21 | +|
| project-directories.ts | +
+ |
+ 100.0 % | +8 | +8 | +|
| project-id.ts | +
+ |
+ 100.0 % | +6 | +6 | +|
| project.ts | +
+ |
+ 100.0 % | +34 | +34 | +|
| prompt.ts | +
+ |
+ 85.7 % | +49 | +42 | +|
| provider.ts | +
+ |
+ 100.0 % | +59 | +59 | +|
| pty-ticket.ts | +
+ |
+ 100.0 % | +7 | +7 | +|
| pty.ts | +
+ |
+ 100.0 % | +47 | +47 | +|
| question.ts | +
+ |
+ 100.0 % | +67 | +67 | +|
| reference.ts | +
+ |
+ 100.0 % | +30 | +30 | +|
| revert.ts | +
+ |
+ 100.0 % | +19 | +19 | +|
| schema.ts | +
+ |
+ 100.0 % | +22 | +22 | +|
| session-delivery.ts | +
+ |
+ 100.0 % | +3 | +3 | +|
| session-event.ts | +
+ |
+ 100.0 % | +429 | +429 | +|
| session-id.ts | +
+ |
+ 100.0 % | +13 | +13 | +|
| session-input.ts | +
+ |
+ 100.0 % | +18 | +18 | +|
| session-message.ts | +
+ |
+ 100.0 % | +170 | +170 | +|
| session-todo.ts | +
+ |
+ 100.0 % | +21 | +21 | +|
| session-v1.ts | +
+ |
+ 100.0 % | +1 | +1 | +|
| session.ts | +
+ |
+ 100.0 % | +43 | +43 | +|
| skill.ts | +
+ |
+ 95.2 % | +42 | +40 | +|
| workspace-event.ts | +
+ |
+ 100.0 % | +25 | +25 | +|
| workspace-id.ts | +
+ |
+ 93.8 % | +16 | +15 | +|
| workspace.ts | +
+ |
+ 100.0 % | +5 | +5 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Schema } from "effect" + 2 : + 3 80 : export const IntegrationID = Schema.String.pipe(Schema.brand("Integration.ID")) + 4 : export type IntegrationID = typeof IntegrationID.Type + 5 : + 6 91 : export const IntegrationMethodID = Schema.String.pipe(Schema.brand("Integration.MethodID")) + 7 : export type IntegrationMethodID = typeof IntegrationMethodID.Type ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 7 : export * as Integration from "./integration" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 44 : import { define, inventory } from "./event" + 6 42 : import { Connection } from "./connection" + 7 41 : import { ascending } from "./identifier" + 8 35 : import { statics } from "./schema" + 9 70 : import { IntegrationID, IntegrationMethodID } from "./integration-id" + 10 : + 11 32 : export const ID = IntegrationID + 12 : export type ID = typeof ID.Type + 13 : + 14 44 : export const MethodID = IntegrationMethodID + 15 : export type MethodID = typeof MethodID.Type + 16 : + 17 : export interface When extends Schema.Schema.Type<typeof When> {} + 18 36 : export const When = Schema.Struct({ + 19 21 : key: Schema.String, + 20 37 : op: Schema.Literals(["eq", "neq"]), + 21 21 : value: Schema.String, + 22 48 : }).annotate({ identifier: "Integration.When" }) + 23 : + 24 : export interface TextPrompt extends Schema.Schema.Type<typeof TextPrompt> {} + 25 42 : export const TextPrompt = Schema.Struct({ + 26 31 : type: Schema.Literal("text"), + 27 21 : key: Schema.String, + 28 25 : message: Schema.String, + 29 39 : placeholder: optional(Schema.String), + 30 21 : when: optional(When), + 31 54 : }).annotate({ identifier: "Integration.TextPrompt" }) + 32 : + 33 : export interface SelectPrompt extends Schema.Schema.Type<typeof SelectPrompt> {} + 34 44 : export const SelectPrompt = Schema.Struct({ + 35 33 : type: Schema.Literal("select"), + 36 21 : key: Schema.String, + 37 25 : message: Schema.String, + 38 22 : options: Schema.Array( + 39 19 : Schema.Struct({ + 40 25 : label: Schema.String, + 41 25 : value: Schema.String, + 42 31 : hint: optional(Schema.String), + 43 2 : }), + 44 4 : ), + 45 21 : when: optional(When), + 46 56 : }).annotate({ identifier: "Integration.SelectPrompt" }) + 47 : + 48 98 : export const Prompt = Schema.Union([TextPrompt, SelectPrompt]).pipe(Schema.toTaggedUnion("type")) + 49 : export type Prompt = typeof Prompt.Type + 50 : + 51 : export interface OAuthMethod extends Schema.Schema.Type<typeof OAuthMethod> {} + 52 43 : export const OAuthMethod = Schema.Struct({ + 53 15 : id: MethodID, + 54 32 : type: Schema.Literal("oauth"), + 55 23 : label: Schema.String, + 56 40 : prompts: optional(Schema.Array(Prompt)), + 57 55 : }).annotate({ identifier: "Integration.OAuthMethod" }) + 58 : + 59 : export interface KeyMethod extends Schema.Schema.Type<typeof KeyMethod> {} + 60 41 : export const KeyMethod = Schema.Struct({ + 61 30 : type: Schema.Literal("key"), + 62 31 : label: optional(Schema.String), + 63 53 : }).annotate({ identifier: "Integration.KeyMethod" }) + 64 : + 65 : export interface EnvMethod extends Schema.Schema.Type<typeof EnvMethod> {} + 66 41 : export const EnvMethod = Schema.Struct({ + 67 30 : type: Schema.Literal("env"), + 68 35 : names: Schema.Array(Schema.String), + 69 53 : }).annotate({ identifier: "Integration.EnvMethod" }) + 70 : + 71 71 : export const Method = Schema.Union([OAuthMethod, KeyMethod, EnvMethod]) + 72 35 : .pipe(Schema.toTaggedUnion("type")) + 73 48 : .annotate({ identifier: "Integration.Method" }) + 74 : export type Method = typeof Method.Type + 75 : + 76 113 : export const Inputs = Schema.Record(Schema.String, Schema.String).annotate({ identifier: "Integration.Inputs" }) + 77 : export type Inputs = typeof Inputs.Type + 78 : + 79 25 : const Updated = define({ + 80 30 : type: "integration.updated", + 81 11 : schema: {}, + 82 3 : }) + 83 35 : const ConnectionUpdated = define({ + 84 41 : type: "integration.connection.updated", + 85 30 : schema: { integrationID: ID }, + 86 3 : }) + 87 104 : export const Event = { Updated, ConnectionUpdated, Definitions: inventory(Updated, ConnectionUpdated) } + 88 : + 89 : export interface Ref extends Schema.Schema.Type<typeof Ref> {} + 90 35 : export const Ref = Schema.Struct({ + 91 9 : id: ID, + 92 20 : name: Schema.String, + 93 47 : }).annotate({ identifier: "Integration.Ref" }) + 94 : + 95 61 : export class Info extends Schema.Class<Info>("Integration.Info")({ + 96 9 : id: ID, + 97 22 : name: Schema.String, + 98 32 : methods: Schema.Array(Method), + 99 43 : connections: Schema.Array(Connection.Info), + 100 5 : }) {} + 101 : + 102 43 : export const AttemptID = Schema.String.pipe( + 103 39 : Schema.brand("Integration.AttemptID"), + 104 70 : statics((schema) => ({ create: () => schema.make("con_" + ascending()) })), + 105 3 : ) + 106 : export type AttemptID = typeof AttemptID.Type + 107 : + 108 36 : const AttemptTime = Schema.Struct({ + 109 25 : created: Schema.Number, + 110 23 : expires: Schema.Number, + 111 3 : }) + 112 : + 113 67 : export class Attempt extends Schema.Class<Attempt>("Integration.Attempt")({ + 114 23 : attemptID: AttemptID, + 115 21 : url: Schema.String, + 116 30 : instructions: Schema.String, + 117 42 : mode: Schema.Literals(["auto", "code"]), + 118 18 : time: AttemptTime, + 119 5 : }) {} + 120 : + 121 44 : export const AttemptStatus = Schema.Union([ + 122 74 : Schema.Struct({ status: Schema.Literal("pending"), time: AttemptTime }), + 123 75 : Schema.Struct({ status: Schema.Literal("complete"), time: AttemptTime }), + 124 97 : Schema.Struct({ status: Schema.Literal("failed"), message: Schema.String, time: AttemptTime }), + 125 72 : Schema.Struct({ status: Schema.Literal("expired"), time: AttemptTime }), + 126 2 : ]) + 127 37 : .pipe(Schema.toTaggedUnion("status")) + 128 54 : .annotate({ identifier: "Integration.AttemptStatus" }) + 129 : export type AttemptStatus = typeof AttemptStatus.Type ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 29 : export * as LLM from "./llm" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 : + 6 118 : export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)).annotate({ + 7 35 : identifier: "LLM.ProviderMetadata", + 8 3 : }) + 9 : export type ProviderMetadata = Schema.Schema.Type<typeof ProviderMetadata> + 10 : + 11 : export interface ToolTextContent extends Schema.Schema.Type<typeof ToolTextContent> {} + 12 47 : export const ToolTextContent = Schema.Struct({ + 13 31 : type: Schema.Literal("text"), + 14 20 : text: Schema.String, + 15 48 : }).annotate({ identifier: "Tool.TextContent" }) + 16 : + 17 : export interface ToolFileContent extends Schema.Schema.Type<typeof ToolFileContent> {} + 18 47 : export const ToolFileContent = Schema.Struct({ + 19 31 : type: Schema.Literal("file"), + 20 21 : uri: Schema.String, + 21 22 : mime: Schema.String, + 22 30 : name: optional(Schema.String), + 23 48 : }).annotate({ identifier: "Tool.FileContent" }) + 24 : + 25 75 : export const ToolContent = Schema.Union([ToolTextContent, ToolFileContent]) + 26 35 : .pipe(Schema.toTaggedUnion("type")) + 27 44 : .annotate({ identifier: "LLM.ToolContent" }) + 28 : export type ToolContent = Schema.Schema.Type<typeof ToolContent> ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 1 : export * as Location from "./location" + 2 : + 3 32 : import { Schema } from "effect" + 4 50 : import { AbsolutePath, optional } from "./schema" + 5 41 : import { ProjectID } from "./project-id" + 6 45 : import { WorkspaceID } from "./workspace-id" + 7 : + 8 : export interface Ref extends Schema.Schema.Type<typeof Ref> {} + 9 35 : export const Ref = Schema.Struct({ + 10 26 : directory: AbsolutePath, + 11 35 : workspaceID: optional(WorkspaceID), + 12 44 : }).annotate({ identifier: "Location.Ref" }) + 13 : + 14 58 : export class Info extends Schema.Class<Info>("Location.Info")({ + 15 26 : directory: AbsolutePath, + 16 37 : workspaceID: optional(WorkspaceID), + 17 28 : project: Schema.Struct({ + 18 18 : id: ProjectID, + 19 25 : directory: AbsolutePath, + 20 3 : }), + 21 5 : }) {} + 22 : + 23 0 : export function response<S extends Schema.Top>(data: S) { + 24 : return Schema.Struct({ location: Info, data }) + 25 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 33 : export * as Model from "./model" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 38 : import { Provider } from "./provider" + 6 35 : import { statics } from "./schema" + 7 : + 8 65 : export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID")) + 9 : export type ID = typeof ID.Type + 10 : + 11 71 : export const VariantID = Schema.String.pipe(Schema.brand("VariantID")) + 12 : export type VariantID = typeof VariantID.Type + 13 : + 14 35 : export const Ref = Schema.Struct({ + 15 9 : id: ID, + 16 26 : providerID: Provider.ID, + 17 34 : variant: VariantID.pipe(optional), + 18 41 : }).annotate({ identifier: "Model.Ref" }) + 19 : export interface Ref extends Schema.Schema.Type<typeof Ref> {} + 20 : + 21 65 : export const Family = Schema.String.pipe(Schema.brand("Family")) + 22 : export type Family = typeof Family.Type + 23 : + 24 : export interface Capabilities extends Schema.Schema.Type<typeof Capabilities> {} + 25 44 : export const Capabilities = Schema.Struct({ + 26 24 : tools: Schema.Boolean, + 27 37 : input: Schema.Array(Schema.String), + 28 36 : output: Schema.Array(Schema.String), + 29 50 : }).annotate({ identifier: "Model.Capabilities" }) + 30 : + 31 : export interface Cost extends Schema.Schema.Type<typeof Cost> {} + 32 36 : export const Cost = Schema.Struct({ + 33 25 : tier: Schema.Struct({ + 34 36 : type: Schema.Literal("context"), + 35 18 : size: Schema.Int, + 36 20 : }).pipe(optional), + 37 23 : input: Schema.Finite, + 38 24 : output: Schema.Finite, + 39 26 : cache: Schema.Struct({ + 40 24 : read: Schema.Finite, + 41 22 : write: Schema.Finite, + 42 3 : }), + 43 42 : }).annotate({ identifier: "Model.Cost" }) + 44 : + 45 34 : export const Api = Schema.Union([ + 46 19 : Schema.Struct({ + 47 14 : id: ID, + 48 23 : ...Provider.AISDK.fields, + 49 5 : }), + 50 19 : Schema.Struct({ + 51 14 : id: ID, + 52 24 : ...Provider.Native.fields, + 53 3 : }), + 54 2 : ]) + 55 35 : .pipe(Schema.toTaggedUnion("type")) + 56 39 : .annotate({ identifier: "Model.Api" }) + 57 : export type Api = typeof Api.Type + 58 : + 59 : export interface Info extends Schema.Schema.Type<typeof Info> {} + 60 36 : export const Info = Schema.Struct({ + 61 9 : id: ID, + 62 26 : providerID: Provider.ID, + 63 32 : family: Family.pipe(optional), + 64 22 : name: Schema.String, + 65 11 : api: Api, + 66 29 : capabilities: Capabilities, + 67 31 : request: Schema.Struct({ + 68 28 : ...Provider.Request.fields, + 69 39 : variant: Schema.String.pipe(optional), + 70 5 : }), + 71 29 : variants: Schema.Struct({ + 72 21 : id: VariantID, + 73 25 : ...Provider.Request.fields, + 74 24 : }).pipe(Schema.Array), + 75 25 : time: Schema.Struct({ + 76 25 : released: Schema.Finite, + 77 5 : }), + 78 27 : cost: Schema.Array(Cost), + 79 69 : status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), + 80 26 : enabled: Schema.Boolean, + 81 26 : limit: Schema.Struct({ + 82 24 : context: Schema.Int, + 83 37 : input: Schema.Int.pipe(optional), + 84 20 : output: Schema.Int, + 85 3 : }), + 86 2 : }) + 87 41 : .annotate({ identifier: "ModelV2.Info" }) + 88 5 : .pipe( + 89 23 : statics((schema) => ({ + 90 31 : empty: (providerID: Provider.ID, modelID: ID) => + 91 17 : schema.make({ + 92 16 : id: modelID, + 93 15 : providerID, + 94 18 : name: modelID, + 95 55 : api: { id: modelID, type: "native", settings: {} }, + 96 58 : capabilities: { tools: false, input: [], output: [] }, + 97 39 : request: { headers: {}, body: {} }, + 98 17 : variants: [], + 99 26 : time: { released: 0 }, + 100 13 : cost: [], + 101 21 : status: "active", + 102 18 : enabled: true, + 103 34 : limit: { context: 0, output: 0 }, + 104 2 : }), + 105 1 : })), + 106 2 : ) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 42 : export * as ModelsDev from "./models-dev" + 2 : + 3 44 : import { define, inventory } from "./event" + 4 : + 5 27 : const Refreshed = define({ + 6 31 : type: "models-dev.refreshed", + 7 11 : schema: {}, + 8 3 : }) + 9 69 : export const Event = { Refreshed, Definitions: inventory(Refreshed) } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 54 : export * as PermissionSaved from "./permission-saved" + 2 : + 3 32 : import { Schema } from "effect" + 4 41 : import { ascending } from "./identifier" + 5 41 : import { ProjectID } from "./project-id" + 6 35 : import { statics } from "./schema" + 7 : + 8 36 : export const ID = Schema.String.pipe( + 9 36 : Schema.brand("PermissionSaved.ID"), + 10 70 : statics((schema) => ({ create: () => schema.make("psv_" + ascending()) })), + 11 3 : ) + 12 : export type ID = typeof ID.Type + 13 : + 14 36 : export const Info = Schema.Struct({ + 15 9 : id: ID, + 16 23 : projectID: ProjectID, + 17 24 : action: Schema.String, + 18 24 : resource: Schema.String, + 19 51 : }).annotate({ identifier: "PermissionSaved.Info" }) + 20 : export interface Info extends Schema.Schema.Type<typeof Info> {} ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+
+ 1 31 : export * from "./v1/permission"
+
+ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 43 : export * as Permission from "./permission" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 44 : import { define, inventory } from "./event" + 6 41 : import { ascending } from "./identifier" + 7 41 : import { SessionID } from "./session-id" + 8 35 : import { statics } from "./schema" + 9 : + 10 70 : export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe( + 11 33 : Schema.brand("PermissionV2.ID"), + 12 78 : statics((schema) => ({ create: (id?: string) => schema.make(id ?? "per_" + ascending()) })), + 13 3 : ) + 14 : export type ID = typeof ID.Type + 15 : + 16 37 : export const Source = Schema.Union([ + 17 19 : Schema.Struct({ + 18 33 : type: Schema.Literal("tool"), + 19 29 : messageID: Schema.String, + 20 23 : callID: Schema.String, + 21 3 : }), + 22 51 : ]).annotate({ identifier: "PermissionV2.Source" }) + 23 : export type Source = typeof Source.Type + 24 : + 25 24 : const RequestFields = { + 26 23 : sessionID: SessionID, + 27 24 : action: Schema.String, + 28 41 : resources: Schema.Array(Schema.String), + 29 51 : save: Schema.Array(Schema.String).pipe(optional), + 30 72 : metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional), + 31 30 : source: Source.pipe(optional), + 32 2 : } + 33 : + 34 39 : export const Request = Schema.Struct({ + 35 12 : id: ID, + 36 14 : ...RequestFields, + 37 52 : }).annotate({ identifier: "PermissionV2.Request" }) + 38 : export interface Request extends Schema.Schema.Type<typeof Request> {} + 39 : + 40 114 : export const Reply = Schema.Literals(["once", "always", "reject"]).annotate({ identifier: "PermissionV2.Reply" }) + 41 : export type Reply = typeof Reply.Type + 42 : + 43 78 : const Asked = define({ type: "permission.v2.asked", schema: Request.fields }) + 44 25 : const Replied = define({ + 45 32 : type: "permission.v2.replied", + 46 13 : schema: { + 47 25 : sessionID: SessionID, + 48 18 : requestID: ID, + 49 14 : reply: Reply, + 50 2 : }, + 51 3 : }) + 52 80 : export const Event = { Asked, Replied, Definitions: inventory(Asked, Replied) } + 53 : + 54 112 : export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Effect" }) + 55 : export type Effect = typeof Effect.Type + 56 : + 57 : export interface Rule extends Schema.Schema.Type<typeof Rule> {} + 58 36 : export const Rule = Schema.Struct({ + 59 24 : action: Schema.String, + 60 26 : resource: Schema.String, + 61 15 : effect: Effect, + 62 49 : }).annotate({ identifier: "PermissionV2.Rule" }) + 63 : + 64 90 : export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV2.Ruleset" }) + 65 : export type Ruleset = typeof Ruleset.Type ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 35 : export * as Plugin from "./plugin" + 2 : + 3 32 : import { Schema } from "effect" + 4 44 : import { define, inventory } from "./event" + 5 : + 6 64 : export const ID = Schema.String.pipe(Schema.brand("Plugin.ID")) + 7 : export type ID = typeof ID.Type + 8 : + 9 23 : const Added = define({ + 10 23 : type: "plugin.added", + 11 19 : schema: { id: ID }, + 12 3 : }) + 13 61 : export const Event = { Added, Definitions: inventory(Added) } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 46 : export * as ProjectCopy from "./project-copy" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 41 : import { ProjectID } from "./project-id" + 6 40 : import { AbsolutePath } from "./schema" + 7 : + 8 118 : export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID")) + 9 : export type StrategyID = typeof StrategyID.Type + 10 : + 11 43 : export const CreateInput = Schema.Struct({ + 12 23 : projectID: ProjectID, + 13 23 : strategy: StrategyID, + 14 32 : sourceDirectory: AbsolutePath, + 15 26 : directory: AbsolutePath, + 16 30 : name: optional(Schema.String), + 17 55 : }).annotate({ identifier: "ProjectCopy.CreateInput" }) + 18 : export interface CreateInput extends Schema.Schema.Type<typeof CreateInput> {} + 19 : + 20 43 : export const RemoveInput = Schema.Struct({ + 21 23 : projectID: ProjectID, + 22 26 : directory: AbsolutePath, + 23 22 : force: Schema.Boolean, + 24 55 : }).annotate({ identifier: "ProjectCopy.RemoveInput" }) + 25 : export interface RemoveInput extends Schema.Schema.Type<typeof RemoveInput> {} + 26 : + 27 36 : export const Copy = Schema.Struct({ + 28 24 : directory: AbsolutePath, + 29 47 : }).annotate({ identifier: "ProjectCopy.Copy" }) + 30 : export interface Copy extends Schema.Schema.Type<typeof Copy> {} ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 60 : export * as ProjectDirectories from "./project-directories" + 2 : + 3 44 : import { define, inventory } from "./event" + 4 36 : import { Project } from "./project" + 5 : + 6 25 : const Updated = define({ + 7 38 : type: "project.directories.updated", + 8 34 : schema: { projectID: Project.ID }, + 9 3 : }) + 10 65 : export const Event = { Updated, Definitions: inventory(Updated) } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Schema } from "effect" + 2 35 : import { statics } from "./schema" + 3 : + 4 43 : export const ProjectID = Schema.String.pipe( + 5 28 : Schema.brand("Project.ID"), + 6 54 : statics((schema) => ({ global: schema.make("global") })), + 7 2 : ) + 8 : export type ProjectID = typeof ProjectID.Type ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 37 : export * as Project from "./project" + 2 : + 3 32 : import { Schema } from "effect" + 4 44 : import { define, inventory } from "./event" + 5 52 : import { NonNegativeInt, optional } from "./schema" + 6 41 : import { ProjectID } from "./project-id" + 7 : + 8 28 : export const ID = ProjectID + 9 : export type ID = typeof ID.Type + 10 : + 11 81 : export const Vcs = Schema.Literal("git").annotate({ identifier: "Project.Vcs" }) + 12 36 : export const Icon = Schema.Struct({ + 13 31 : url: optional(Schema.String), + 14 36 : override: optional(Schema.String), + 15 31 : color: optional(Schema.String), + 16 44 : }).annotate({ identifier: "Project.Icon" }) + 17 : export interface Icon extends Schema.Schema.Type<typeof Icon> {} + 18 40 : export const Commands = Schema.Struct({ + 19 16 : start: optional( + 20 105 : Schema.String.annotate({ description: "Startup script to run when creating a new workspace (worktree)" }), + 21 2 : ), + 22 48 : }).annotate({ identifier: "Project.Commands" }) + 23 : export interface Commands extends Schema.Schema.Type<typeof Commands> {} + 24 36 : export const Time = Schema.Struct({ + 25 26 : created: NonNegativeInt, + 26 26 : updated: NonNegativeInt, + 27 38 : initialized: optional(NonNegativeInt), + 28 44 : }).annotate({ identifier: "Project.Time" }) + 29 : export interface Time extends Schema.Schema.Type<typeof Time> {} + 30 : + 31 36 : export const Info = Schema.Struct({ + 32 9 : id: ID, + 33 26 : worktree: Schema.String, + 34 21 : vcs: optional(Vcs), + 35 32 : name: optional(Schema.String), + 36 23 : icon: optional(Icon), + 37 31 : commands: optional(Commands), + 38 13 : time: Time, + 39 39 : sandboxes: Schema.Array(Schema.String), + 40 39 : }).annotate({ identifier: "Project" }) + 41 : export interface Info extends Schema.Schema.Type<typeof Info> {} + 42 : + 43 73 : const Updated = define({ type: "project.updated", schema: Info.fields }) + 44 65 : export const Event = { Updated, Definitions: inventory(Updated) } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Schema } from "effect" + 2 36 : import { optional } from "./schema" + 3 35 : import { statics } from "./schema" + 4 : + 5 : export interface Source extends Schema.Schema.Type<typeof Source> {} + 6 38 : export const Source = Schema.Struct({ + 7 23 : start: Schema.Finite, + 8 21 : end: Schema.Finite, + 9 20 : text: Schema.String, + 10 45 : }).annotate({ identifier: "Prompt.Source" }) + 11 : + 12 : export interface FileAttachment extends Schema.Schema.Type<typeof FileAttachment> {} + 13 46 : export const FileAttachment = Schema.Struct({ + 14 21 : uri: Schema.String, + 15 22 : mime: Schema.String, + 16 37 : name: Schema.String.pipe(optional), + 17 44 : description: Schema.String.pipe(optional), + 18 30 : source: Source.pipe(optional), + 19 2 : }) + 20 50 : .annotate({ identifier: "Prompt.FileAttachment" }) + 21 5 : .pipe( + 22 23 : statics((schema) => ({ + 23 0 : create: (input: FileAttachment) => + 24 0 : schema.make({ + 25 0 : uri: input.uri, + 26 0 : mime: input.mime, + 27 0 : name: input.name, + 28 0 : description: input.description, + 29 0 : source: input.source, + 30 1 : }), + 31 1 : })), + 32 3 : ) + 33 : + 34 : export interface AgentAttachment extends Schema.Schema.Type<typeof AgentAttachment> {} + 35 47 : export const AgentAttachment = Schema.Struct({ + 36 22 : name: Schema.String, + 37 30 : source: Source.pipe(optional), + 38 54 : }).annotate({ identifier: "Prompt.AgentAttachment" }) + 39 : + 40 : export interface Prompt extends Schema.Schema.Type<typeof Prompt> {} + 41 38 : export const Prompt = Schema.Struct({ + 42 22 : text: Schema.String, + 43 53 : files: Schema.Array(FileAttachment).pipe(optional), + 44 53 : agents: Schema.Array(AgentAttachment).pipe(optional), + 45 2 : }) + 46 35 : .annotate({ identifier: "Prompt" }) + 47 5 : .pipe( + 48 23 : statics((schema) => ({ + 49 44 : equivalence: Schema.toEquivalence(schema), + 50 27 : fromUserMessage: (input: Pick<Prompt, "text" | "files" | "agents">) => + 51 17 : schema.make({ + 52 24 : text: input.text, + 53 36 : ...(input.files === undefined ? {} : { files: input.files }), + 54 31 : ...(input.agents === undefined ? {} : { agents: input.agents }), + 55 2 : }), + 56 1 : })), + 57 2 : ) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 39 : export * as Provider from "./provider" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 44 : import { Integration } from "./integration" + 6 35 : import { statics } from "./schema" + 7 : + 8 36 : export const ID = Schema.String.pipe( + 9 31 : Schema.brand("ProviderV2.ID"), + 10 23 : statics((schema) => ({ + 11 36 : opencode: schema.make("opencode"), + 12 38 : anthropic: schema.make("anthropic"), + 13 32 : openai: schema.make("openai"), + 14 32 : google: schema.make("google"), + 15 45 : googleVertex: schema.make("google-vertex"), + 16 47 : githubCopilot: schema.make("github-copilot"), + 17 47 : amazonBedrock: schema.make("amazon-bedrock"), + 18 30 : azure: schema.make("azure"), + 19 40 : openrouter: schema.make("openrouter"), + 20 34 : mistral: schema.make("mistral"), + 21 30 : gitlab: schema.make("gitlab"), + 22 1 : })), + 23 3 : ) + 24 : export type ID = typeof ID.Type + 25 : + 26 : export interface AISDK extends Schema.Schema.Type<typeof AISDK> {} + 27 37 : export const AISDK = Schema.Struct({ + 28 32 : type: Schema.Literal("aisdk"), + 29 25 : package: Schema.String, + 30 36 : url: Schema.String.pipe(optional), + 31 70 : settings: Schema.Record(Schema.String, Schema.Unknown).pipe(optional), + 32 46 : }).annotate({ identifier: "Provider.AISDK" }) + 33 : + 34 : export interface Native extends Schema.Schema.Type<typeof Native> {} + 35 38 : export const Native = Schema.Struct({ + 36 33 : type: Schema.Literal("native"), + 37 36 : url: Schema.String.pipe(optional), + 38 55 : settings: Schema.Record(Schema.String, Schema.Unknown), + 39 47 : }).annotate({ identifier: "Provider.Native" }) + 40 : + 41 48 : export const Api = Schema.Union([AISDK, Native]) + 42 35 : .pipe(Schema.toTaggedUnion("type")) + 43 42 : .annotate({ identifier: "Provider.Api" }) + 44 : export type Api = typeof Api.Type + 45 : + 46 : export interface Request extends Schema.Schema.Type<typeof Request> {} + 47 39 : export const Request = Schema.Struct({ + 48 55 : headers: Schema.Record(Schema.String, Schema.String), + 49 48 : body: Schema.Record(Schema.String, Schema.Json), + 50 48 : }).annotate({ identifier: "Provider.Request" }) + 51 : + 52 : export interface Info extends Schema.Schema.Type<typeof Info> {} + 53 36 : export const Info = Schema.Struct({ + 54 9 : id: ID, + 55 47 : integrationID: Integration.ID.pipe(optional), + 56 22 : name: Schema.String, + 57 42 : disabled: Schema.Boolean.pipe(optional), + 58 11 : api: Api, + 59 17 : request: Request, + 60 2 : }) + 61 44 : .annotate({ identifier: "ProviderV2.Info" }) + 62 5 : .pipe( + 63 23 : statics((schema) => ({ + 64 14 : empty: (id: ID) => + 65 17 : schema.make({ + 66 7 : id, + 67 13 : name: id, + 68 42 : api: { type: "native", settings: {} }, + 69 36 : request: { headers: {}, body: {} }, + 70 2 : }), + 71 1 : })), + 72 2 : ) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 42 : export * as PtyTicket from "./pty-ticket" + 2 : + 3 32 : import { Schema } from "effect" + 4 39 : import { PositiveInt } from "./schema" + 5 : + 6 44 : export const ConnectToken = Schema.Struct({ + 7 24 : ticket: Schema.String, + 8 24 : expires_in: PositiveInt, + 9 53 : }).annotate({ identifier: "PtyTicket.ConnectToken" }) + 10 : export interface ConnectToken extends Schema.Schema.Type<typeof ConnectToken> {} ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 29 : export * as Pty from "./pty" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 44 : import { define, inventory } from "./event" + 6 41 : import { ascending } from "./identifier" + 7 64 : import { NonNegativeInt, PositiveInt, statics } from "./schema" + 8 : + 9 93 : const IDSchema = Schema.String.check(Schema.isStartsWith("pty")).pipe(Schema.brand("PtyID")) + 10 : + 11 31 : export const ID = IDSchema.pipe( + 12 22 : statics((schema: typeof IDSchema) => { + 13 55 : const create = () => schema.make("pty_" + ascending()) + 14 12 : return { + 15 11 : create, + 16 46 : ascending: (id?: string) => (id === undefined ? create() : schema.make(id)), + 17 1 : } + 18 1 : }), + 19 3 : ) + 20 : export type ID = typeof ID.Type + 21 : + 22 36 : export const Info = Schema.Struct({ + 23 9 : id: ID, + 24 23 : title: Schema.String, + 25 25 : command: Schema.String, + 26 36 : args: Schema.Array(Schema.String), + 27 21 : cwd: Schema.String, + 28 49 : status: Schema.Literals(["running", "exited"]), + 29 22 : pid: NonNegativeInt, + 30 35 : exitCode: optional(NonNegativeInt), + 31 35 : }).annotate({ identifier: "Pty" }) + 32 : export interface Info extends Schema.Schema.Type<typeof Info> {} + 33 : + 34 72 : const Created = define({ type: "pty.created", schema: { info: Info } }) + 35 72 : const Updated = define({ type: "pty.updated", schema: { info: Info } }) + 36 92 : const Exited = define({ type: "pty.exited", schema: { id: ID, exitCode: NonNegativeInt } }) + 37 68 : const Deleted = define({ type: "pty.deleted", schema: { id: ID } }) + 38 118 : export const Event = { Created, Updated, Exited, Deleted, Definitions: inventory(Created, Updated, Exited, Deleted) } + 39 : + 40 43 : export const CreateInput = Schema.Struct({ + 41 35 : command: optional(Schema.String), + 42 46 : args: optional(Schema.Array(Schema.String)), + 43 31 : cwd: optional(Schema.String), + 44 33 : title: optional(Schema.String), + 45 59 : env: optional(Schema.Record(Schema.String, Schema.String)), + 46 3 : }) + 47 : export interface CreateInput extends Schema.Schema.Type<typeof CreateInput> {} + 48 : + 49 43 : export const UpdateInput = Schema.Struct({ + 50 33 : title: optional(Schema.String), + 51 15 : size: optional( + 52 19 : Schema.Struct({ + 53 22 : rows: PositiveInt, + 54 19 : cols: PositiveInt, + 55 2 : }), + 56 2 : ), + 57 2 : }) + 58 : export interface UpdateInput extends Schema.Schema.Type<typeof UpdateInput> {} ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 39 : export * as Question from "./question" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 44 : import { define, inventory } from "./event" + 6 41 : import { ascending } from "./identifier" + 7 41 : import { SessionID } from "./session-id" + 8 35 : import { statics } from "./schema" + 9 : + 10 70 : export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe( + 11 31 : Schema.brand("QuestionV2.ID"), + 12 22 : statics((schema) => { + 13 55 : const create = () => schema.make("que_" + ascending()) + 14 12 : return { + 15 11 : create, + 16 63 : ascending: (id?: string) => (id === undefined ? create() : schema.make(id)), + 17 1 : } + 18 1 : }), + 19 3 : ) + 20 : export type ID = typeof ID.Type + 21 : + 22 38 : export const Option = Schema.Struct({ + 23 86 : label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }), + 24 78 : description: Schema.String.annotate({ description: "Explanation of choice" }), + 25 49 : }).annotate({ identifier: "QuestionV2.Option" }) + 26 : export interface Option extends Schema.Schema.Type<typeof Option> {} + 27 : + 28 15 : const base = { + 29 73 : question: Schema.String.annotate({ description: "Complete question" }), + 30 85 : header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }), + 31 79 : options: Schema.Array(Option).annotate({ description: "Available choices" }), + 32 102 : multiple: Schema.Boolean.pipe(optional).annotate({ description: "Allow selecting multiple choices" }), + 33 2 : } + 34 : + 35 39 : export const Info = Schema.Struct({ + 36 7 : ...base, + 37 52 : custom: Schema.Boolean.pipe(optional).annotate({ + 38 61 : description: "Allow typing a custom answer (default: true)", + 39 3 : }), + 40 47 : }).annotate({ identifier: "QuestionV2.Info" }) + 41 : export interface Info extends Schema.Schema.Type<typeof Info> {} + 42 : + 43 88 : export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV2.Prompt" }) + 44 : export interface Prompt extends Schema.Schema.Type<typeof Prompt> {} + 45 : + 46 36 : export const Tool = Schema.Struct({ + 47 27 : messageID: Schema.String, + 48 22 : callID: Schema.String, + 49 47 : }).annotate({ identifier: "QuestionV2.Tool" }) + 50 : export interface Tool extends Schema.Schema.Type<typeof Tool> {} + 51 : + 52 39 : export const Request = Schema.Struct({ + 53 9 : id: ID, + 54 23 : sessionID: SessionID, + 55 78 : questions: Schema.Array(Info).annotate({ description: "Questions to ask" }), + 56 26 : tool: Tool.pipe(optional), + 57 50 : }).annotate({ identifier: "QuestionV2.Request" }) + 58 : export interface Request extends Schema.Schema.Type<typeof Request> {} + 59 : + 60 96 : export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV2.Answer" }) + 61 : export type Answer = typeof Answer.Type + 62 : + 63 37 : export const Reply = Schema.Struct({ + 64 44 : answers: Schema.Array(Answer).annotate({ + 65 96 : description: "User answers in order of questions (each answer is an array of selected labels)", + 66 3 : }), + 67 48 : }).annotate({ identifier: "QuestionV2.Reply" }) + 68 : export interface Reply extends Schema.Schema.Type<typeof Reply> {} + 69 : + 70 76 : const Asked = define({ type: "question.v2.asked", schema: Request.fields }) + 71 25 : const Replied = define({ + 72 30 : type: "question.v2.replied", + 73 13 : schema: { + 74 25 : sessionID: SessionID, + 75 18 : requestID: ID, + 76 31 : answers: Schema.Array(Answer), + 77 2 : }, + 78 3 : }) + 79 26 : const Rejected = define({ + 80 31 : type: "question.v2.rejected", + 81 13 : schema: { + 82 25 : sessionID: SessionID, + 83 15 : requestID: ID, + 84 2 : }, + 85 3 : }) + 86 99 : export const Event = { Asked, Replied, Rejected, Definitions: inventory(Asked, Replied, Rejected) } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 3 : export * as Reference from "./reference" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 44 : import { define, inventory } from "./event" + 6 40 : import { AbsolutePath } from "./schema" + 7 : + 8 66 : const Updated = define({ type: "reference.updated", schema: {} }) + 9 66 : export const Event = { Updated, Definitions: inventory(Updated) } + 10 : + 11 : export interface LocalSource extends Schema.Schema.Type<typeof LocalSource> {} + 12 43 : export const LocalSource = Schema.Struct({ + 13 32 : type: Schema.Literal("local"), + 14 21 : path: AbsolutePath, + 15 44 : description: Schema.String.pipe(optional), + 16 38 : hidden: Schema.Boolean.pipe(optional), + 17 53 : }).annotate({ identifier: "Reference.LocalSource" }) + 18 : + 19 : export interface GitSource extends Schema.Schema.Type<typeof GitSource> {} + 20 41 : export const GitSource = Schema.Struct({ + 21 30 : type: Schema.Literal("git"), + 22 28 : repository: Schema.String, + 23 39 : branch: Schema.String.pipe(optional), + 24 44 : description: Schema.String.pipe(optional), + 25 38 : hidden: Schema.Boolean.pipe(optional), + 26 51 : }).annotate({ identifier: "Reference.GitSource" }) + 27 : + 28 60 : export const Source = Schema.Union([LocalSource, GitSource]) + 29 35 : .pipe(Schema.toTaggedUnion("type")) + 30 46 : .annotate({ identifier: "Reference.Source" }) + 31 : export type Source = typeof Source.Type + 32 : + 33 59 : export class Info extends Schema.Class<Info>("Reference.Info")({ + 34 22 : name: Schema.String, + 35 21 : path: AbsolutePath, + 36 44 : description: Schema.String.pipe(optional), + 37 40 : hidden: Schema.Boolean.pipe(optional), + 38 15 : source: Source, + 39 4 : }) {} ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 35 : export * as Revert from "./revert" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 56 : import { NonNegativeInt, RelativePath } from "./schema" + 6 51 : import { SessionMessage } from "./session-message" + 7 : + 8 40 : export const FileDiff = Schema.Struct({ + 9 21 : path: RelativePath, + 10 60 : status: Schema.Literals(["added", "modified", "deleted"]), + 11 28 : additions: NonNegativeInt, + 12 28 : deletions: NonNegativeInt, + 13 21 : patch: Schema.String, + 14 41 : }).annotate({ identifier: "File.Diff" }) + 15 : export interface FileDiff extends Schema.Schema.Type<typeof FileDiff> {} + 16 : + 17 37 : export const State = Schema.Struct({ + 18 31 : messageID: SessionMessage.ID, + 19 39 : partID: Schema.String.pipe(optional), + 20 41 : snapshot: Schema.String.pipe(optional), + 21 37 : diff: Schema.String.pipe(optional), + 22 45 : files: Schema.Array(FileDiff).pipe(optional), + 23 43 : }).annotate({ identifier: "Revert.State" }) + 24 : export interface State extends Schema.Schema.Type<typeof State> {} ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 64 : import { DateTime, Option, Schema, SchemaGetter } from "effect" + 2 : + 3 69 : export const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0)) + 4 81 : export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) + 5 : + 6 77 : export const RelativePath = Schema.String.pipe(Schema.brand("RelativePath")) + 7 : export type RelativePath = typeof RelativePath.Type + 8 : + 9 77 : export const AbsolutePath = Schema.String.pipe(Schema.brand("AbsolutePath")) + 10 : export type AbsolutePath = typeof AbsolutePath.Type + 11 : + 12 34 : export const optional = <S extends Schema.Top>(schema: S) => + 13 32 : Schema.optionalKey(schema).pipe( + 14 59 : Schema.decodeTo(Schema.optional(Schema.toType(schema)), { + 15 54 : decode: SchemaGetter.passthrough({ strict: false }), + 16 84 : encode: SchemaGetter.transformOptional(Option.filter((value) => value !== undefined)), + 17 1 : }), + 18 2 : ) + 19 : + 20 21 : export const statics = + 21 12 : <S extends object, M extends Record<string, unknown>>(methods: (schema: S) => M) => + 22 12 : (schema: S): S & M => + 23 40 : Object.assign(schema, methods(schema)) + 24 : + 25 55 : export const DateTimeUtcFromMillis = Schema.Finite.pipe( + 26 39 : Schema.decodeTo(Schema.DateTimeUtc, { + 27 70 : decode: SchemaGetter.transform((value) => DateTime.makeUnsafe(value)), + 28 71 : encode: SchemaGetter.transform((value) => DateTime.toEpochMillis(value)), + 29 1 : }), + 30 2 : ) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 54 : export * as SessionDelivery from "./session-delivery" + 2 : + 3 32 : import { Schema } from "effect" + 4 : + 5 59 : export const Delivery = Schema.Literals(["steer", "queue"]) + 6 : export type Delivery = typeof Delivery.Type ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 48 : export * as SessionEvent from "./session-event" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 32 : import { Event } from "./event" + 6 54 : import { ProviderMetadata, ToolContent } from "./llm" + 7 46 : import { Delivery } from "./session-delivery" + 8 32 : import { Model } from "./model" + 9 79 : import { DateTimeUtcFromMillis, NonNegativeInt, RelativePath } from "./schema" + 10 50 : import { FileAttachment, Prompt } from "./prompt" + 11 41 : import { SessionID } from "./session-id" + 12 38 : import { Location } from "./location" + 13 51 : import { SessionMessage } from "./session-message" + 14 34 : import { Revert } from "./revert" + 15 : + 16 26 : export { FileAttachment } + 17 : + 18 38 : export const Source = Schema.Struct({ + 19 24 : start: NonNegativeInt, + 20 22 : end: NonNegativeInt, + 21 20 : text: Schema.String, + 22 14 : }).annotate({ + 23 40 : identifier: "session.next.event.source", + 24 3 : }) + 25 : export interface Source extends Schema.Schema.Type<typeof Source> {} + 26 : + 27 15 : const Base = { + 28 35 : timestamp: DateTimeUtcFromMillis, + 29 21 : sessionID: SessionID, + 30 2 : } + 31 26 : const PromptFields = { + 32 7 : ...Base, + 33 31 : messageID: SessionMessage.ID, + 34 17 : prompt: Prompt, + 35 19 : delivery: Delivery, + 36 2 : } + 37 : + 38 18 : const options = { + 39 14 : durable: { + 40 27 : aggregate: "sessionID", + 41 12 : version: 1, + 42 2 : }, + 43 2 : } as const + 44 32 : const stepSettlementOptions = { + 45 14 : durable: { + 46 27 : aggregate: "sessionID", + 47 12 : version: 2, + 48 2 : }, + 49 2 : } as const + 50 : + 51 56 : export const UnknownError = SessionMessage.UnknownError + 52 : export type UnknownError = SessionMessage.UnknownError + 53 : + 54 44 : export const AgentSwitched = Event.define({ + 55 41 : type: "session.next.agent.switched", + 56 10 : ...options, + 57 16 : schema: { + 58 9 : ...Base, + 59 33 : messageID: SessionMessage.ID, + 60 22 : agent: Schema.String, + 61 2 : }, + 62 3 : }) + 63 : export type AgentSwitched = typeof AgentSwitched.Type + 64 : + 65 44 : export const ModelSwitched = Event.define({ + 66 41 : type: "session.next.model.switched", + 67 10 : ...options, + 68 16 : schema: { + 69 9 : ...Base, + 70 33 : messageID: SessionMessage.ID, + 71 18 : model: Model.Ref, + 72 2 : }, + 73 3 : }) + 74 : export type ModelSwitched = typeof ModelSwitched.Type + 75 : + 76 36 : export const Moved = Event.define({ + 77 32 : type: "session.next.moved", + 78 10 : ...options, + 79 16 : schema: { + 80 9 : ...Base, + 81 27 : location: Location.Ref, + 82 43 : subdirectory: RelativePath.pipe(optional), + 83 2 : }, + 84 3 : }) + 85 : export type Moved = typeof Moved.Type + 86 : + 87 39 : export const Prompted = Event.define({ + 88 35 : type: "session.next.prompted", + 89 10 : ...options, + 90 21 : schema: PromptFields, + 91 3 : }) + 92 : export type Prompted = typeof Prompted.Type + 93 : + 94 45 : export const PromptAdmitted = Event.define({ + 95 42 : type: "session.next.prompt.admitted", + 96 10 : ...options, + 97 21 : schema: PromptFields, + 98 3 : }) + 99 : export type PromptAdmitted = typeof PromptAdmitted.Type + 100 : + 101 45 : export const ContextUpdated = Event.define({ + 102 42 : type: "session.next.context.updated", + 103 10 : ...options, + 104 16 : schema: { + 105 9 : ...Base, + 106 33 : messageID: SessionMessage.ID, + 107 21 : text: Schema.String, + 108 2 : }, + 109 3 : }) + 110 : export type ContextUpdated = typeof ContextUpdated.Type + 111 : + 112 40 : export const Synthetic = Event.define({ + 113 36 : type: "session.next.synthetic", + 114 10 : ...options, + 115 16 : schema: { + 116 9 : ...Base, + 117 33 : messageID: SessionMessage.ID, + 118 21 : text: Schema.String, + 119 2 : }, + 120 3 : }) + 121 : export type Synthetic = typeof Synthetic.Type + 122 : + 123 46 : export namespace Shell { + 124 34 : export const Started = Event.define({ + 125 42 : type: "session.next.shell.started", + 126 12 : ...options, + 127 18 : schema: { + 128 11 : ...Base, + 129 35 : messageID: SessionMessage.ID, + 130 28 : callID: Schema.String, + 131 26 : command: Schema.String, + 132 3 : }, + 133 5 : }) + 134 : export type Started = typeof Started.Type + 135 : + 136 32 : export const Ended = Event.define({ + 137 40 : type: "session.next.shell.ended", + 138 12 : ...options, + 139 18 : schema: { + 140 11 : ...Base, + 141 28 : callID: Schema.String, + 142 25 : output: Schema.String, + 143 3 : }, + 144 6 : }) + 145 : export type Ended = typeof Ended.Type + 146 : } + 147 : + 148 43 : export namespace Step { + 149 33 : export const Started = Event.define({ + 150 41 : type: "session.next.step.started", + 151 12 : ...options, + 152 18 : schema: { + 153 11 : ...Base, + 154 44 : assistantMessageID: SessionMessage.ID, + 155 27 : agent: Schema.String, + 156 23 : model: Model.Ref, + 157 42 : snapshot: Schema.String.pipe(optional), + 158 3 : }, + 159 5 : }) + 160 : export type Started = typeof Started.Type + 161 : + 162 31 : export const Ended = Event.define({ + 163 39 : type: "session.next.step.ended", + 164 26 : ...stepSettlementOptions, + 165 18 : schema: { + 166 11 : ...Base, + 167 44 : assistantMessageID: SessionMessage.ID, + 168 28 : finish: Schema.String, + 169 26 : cost: Schema.Finite, + 170 31 : tokens: Schema.Struct({ + 171 29 : input: Schema.Finite, + 172 30 : output: Schema.Finite, + 173 33 : reasoning: Schema.Finite, + 174 32 : cache: Schema.Struct({ + 175 30 : read: Schema.Finite, + 176 28 : write: Schema.Finite, + 177 8 : }), + 178 9 : }), + 179 45 : snapshot: Schema.String.pipe(optional), + 180 52 : files: Schema.Array(RelativePath).pipe(optional), + 181 3 : }, + 182 5 : }) + 183 : export type Ended = typeof Ended.Type + 184 : + 185 32 : export const Failed = Event.define({ + 186 40 : type: "session.next.step.failed", + 187 26 : ...stepSettlementOptions, + 188 18 : schema: { + 189 11 : ...Base, + 190 44 : assistantMessageID: SessionMessage.ID, + 191 23 : error: UnknownError, + 192 3 : }, + 193 6 : }) + 194 : export type Failed = typeof Failed.Type + 195 : } + 196 : + 197 43 : export namespace Text { + 198 33 : export const Started = Event.define({ + 199 41 : type: "session.next.text.started", + 200 12 : ...options, + 201 18 : schema: { + 202 11 : ...Base, + 203 44 : assistantMessageID: SessionMessage.ID, + 204 25 : textID: Schema.String, + 205 3 : }, + 206 5 : }) + 207 : export type Started = typeof Started.Type + 208 : + 209 : // Stream fragments are live-only; Text.Ended is the replayable full-value boundary. + 210 31 : export const Delta = Event.define({ + 211 36 : type: "session.next.text.delta", + 212 18 : schema: { + 213 11 : ...Base, + 214 44 : assistantMessageID: SessionMessage.ID, + 215 28 : textID: Schema.String, + 216 24 : delta: Schema.String, + 217 3 : }, + 218 5 : }) + 219 : export type Delta = typeof Delta.Type + 220 : + 221 31 : export const Ended = Event.define({ + 222 39 : type: "session.next.text.ended", + 223 12 : ...options, + 224 18 : schema: { + 225 11 : ...Base, + 226 44 : assistantMessageID: SessionMessage.ID, + 227 28 : textID: Schema.String, + 228 23 : text: Schema.String, + 229 3 : }, + 230 6 : }) + 231 : export type Ended = typeof Ended.Type + 232 : } + 233 : + 234 58 : export namespace Reasoning { + 235 38 : export const Started = Event.define({ + 236 46 : type: "session.next.reasoning.started", + 237 12 : ...options, + 238 18 : schema: { + 239 11 : ...Base, + 240 44 : assistantMessageID: SessionMessage.ID, + 241 33 : reasoningID: Schema.String, + 242 53 : providerMetadata: ProviderMetadata.pipe(optional), + 243 3 : }, + 244 5 : }) + 245 : export type Started = typeof Started.Type + 246 : + 247 : // Stream fragments are live-only; Reasoning.Ended is the replayable full-value boundary. + 248 36 : export const Delta = Event.define({ + 249 41 : type: "session.next.reasoning.delta", + 250 18 : schema: { + 251 11 : ...Base, + 252 44 : assistantMessageID: SessionMessage.ID, + 253 33 : reasoningID: Schema.String, + 254 24 : delta: Schema.String, + 255 3 : }, + 256 5 : }) + 257 : export type Delta = typeof Delta.Type + 258 : + 259 36 : export const Ended = Event.define({ + 260 44 : type: "session.next.reasoning.ended", + 261 12 : ...options, + 262 18 : schema: { + 263 11 : ...Base, + 264 44 : assistantMessageID: SessionMessage.ID, + 265 33 : reasoningID: Schema.String, + 266 26 : text: Schema.String, + 267 53 : providerMetadata: ProviderMetadata.pipe(optional), + 268 3 : }, + 269 6 : }) + 270 : export type Ended = typeof Ended.Type + 271 : } + 272 : + 273 43 : export namespace Tool { + 274 25 : const ToolBase = { + 275 9 : ...Base, + 276 42 : assistantMessageID: SessionMessage.ID, + 277 23 : callID: Schema.String, + 278 4 : } + 279 : + 280 57 : export namespace Input { + 281 36 : export const Started = Event.define({ + 282 49 : type: "session.next.tool.input.started", + 283 14 : ...options, + 284 20 : schema: { + 285 17 : ...ToolBase, + 286 25 : name: Schema.String, + 287 5 : }, + 288 7 : }) + 289 : export type Started = typeof Started.Type + 290 : + 291 : // Stream fragments are live-only; Input.Ended is the replayable raw-input boundary. + 292 34 : export const Delta = Event.define({ + 293 44 : type: "session.next.tool.input.delta", + 294 20 : schema: { + 295 17 : ...ToolBase, + 296 26 : delta: Schema.String, + 297 5 : }, + 298 7 : }) + 299 : export type Delta = typeof Delta.Type + 300 : + 301 34 : export const Ended = Event.define({ + 302 47 : type: "session.next.tool.input.ended", + 303 14 : ...options, + 304 20 : schema: { + 305 17 : ...ToolBase, + 306 25 : text: Schema.String, + 307 5 : }, + 308 7 : }) + 309 : export type Ended = typeof Ended.Type + 310 : } + 311 : + 312 32 : export const Called = Event.define({ + 313 40 : type: "session.next.tool.called", + 314 12 : ...options, + 315 18 : schema: { + 316 15 : ...ToolBase, + 317 26 : tool: Schema.String, + 318 58 : input: Schema.Record(Schema.String, Schema.Unknown), + 319 33 : provider: Schema.Struct({ + 320 33 : executed: Schema.Boolean, + 321 47 : metadata: ProviderMetadata.pipe(optional), + 322 6 : }), + 323 3 : }, + 324 5 : }) + 325 : export type Called = typeof Called.Type + 326 : + 327 : /** + 328 : * Replayable bounded running-tool state. Tools should checkpoint semantic + 329 : * transitions or at a bounded cadence, not persist every stdout/stderr chunk. + 330 : */ + 331 34 : export const Progress = Event.define({ + 332 42 : type: "session.next.tool.progress", + 333 12 : ...options, + 334 18 : schema: { + 335 15 : ...ToolBase, + 336 63 : structured: Schema.Record(Schema.String, Schema.Unknown), + 337 38 : content: Schema.Array(ToolContent), + 338 3 : }, + 339 5 : }) + 340 : export type Progress = typeof Progress.Type + 341 : + 342 33 : export const Success = Event.define({ + 343 41 : type: "session.next.tool.success", + 344 12 : ...options, + 345 18 : schema: { + 346 15 : ...ToolBase, + 347 63 : structured: Schema.Record(Schema.String, Schema.Unknown), + 348 41 : content: Schema.Array(ToolContent), + 349 62 : outputPaths: Schema.Array(Schema.String).pipe(optional), + 350 44 : result: Schema.Unknown.pipe(optional), + 351 33 : provider: Schema.Struct({ + 352 33 : executed: Schema.Boolean, + 353 47 : metadata: ProviderMetadata.pipe(optional), + 354 6 : }), + 355 3 : }, + 356 5 : }) + 357 : export type Success = typeof Success.Type + 358 : + 359 32 : export const Failed = Event.define({ + 360 40 : type: "session.next.tool.failed", + 361 12 : ...options, + 362 18 : schema: { + 363 15 : ...ToolBase, + 364 26 : error: UnknownError, + 365 44 : result: Schema.Unknown.pipe(optional), + 366 33 : provider: Schema.Struct({ + 367 33 : executed: Schema.Boolean, + 368 47 : metadata: ProviderMetadata.pipe(optional), + 369 6 : }), + 370 3 : }, + 371 6 : }) + 372 : export type Failed = typeof Failed.Type + 373 : } + 374 : + 375 42 : export const RetryError = Schema.Struct({ + 376 25 : message: Schema.String, + 377 43 : statusCode: Schema.Finite.pipe(optional), + 378 30 : isRetryable: Schema.Boolean, + 379 78 : responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(optional), + 380 45 : responseBody: Schema.String.pipe(optional), + 381 69 : metadata: Schema.Record(Schema.String, Schema.String).pipe(optional), + 382 14 : }).annotate({ + 383 39 : identifier: "session.next.retry_error", + 384 3 : }) + 385 : export interface RetryError extends Schema.Schema.Type<typeof RetryError> {} + 386 : + 387 38 : export const Retried = Event.define({ + 388 34 : type: "session.next.retried", + 389 10 : ...options, + 390 16 : schema: { + 391 9 : ...Base, + 392 27 : attempt: Schema.Finite, + 393 19 : error: RetryError, + 394 2 : }, + 395 3 : }) + 396 : export type Retried = typeof Retried.Type + 397 : + 398 61 : export namespace Compaction { + 399 39 : export const Started = Event.define({ + 400 47 : type: "session.next.compaction.started", + 401 12 : ...options, + 402 18 : schema: { + 403 11 : ...Base, + 404 35 : messageID: SessionMessage.ID, + 405 76 : reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]), + 406 3 : }, + 407 5 : }) + 408 : export type Started = typeof Started.Type + 409 : + 410 37 : export const Delta = Event.define({ + 411 42 : type: "session.next.compaction.delta", + 412 18 : schema: { + 413 11 : ...Base, + 414 35 : messageID: SessionMessage.ID, + 415 23 : text: Schema.String, + 416 3 : }, + 417 5 : }) + 418 : export type Delta = typeof Delta.Type + 419 : + 420 37 : export const Ended = Event.define({ + 421 45 : type: "session.next.compaction.ended", + 422 12 : ...options, + 423 18 : schema: { + 424 11 : ...Base, + 425 35 : messageID: SessionMessage.ID, + 426 52 : reason: Started.data.fields.reason, + 427 26 : text: Schema.String, + 428 25 : recent: Schema.String, + 429 3 : }, + 430 6 : }) + 431 : export type Ended = typeof Ended.Type + 432 : } + 433 : + 434 64 : export namespace RevertEvent { + 435 39 : export const Staged = Event.define({ + 436 42 : type: "session.next.revert.staged", + 437 12 : ...options, + 438 43 : schema: { ...Base, revert: Revert.State }, + 439 5 : }) + 440 104 : export const Cleared = Event.define({ type: "session.next.revert.cleared", ...options, schema: Base }) + 441 42 : export const Committed = Event.define({ + 442 45 : type: "session.next.revert.committed", + 443 12 : ...options, + 444 51 : schema: { ...Base, messageID: SessionMessage.ID }, + 445 6 : }) + 446 : } + 447 : + 448 49 : export const DurableDefinitions = Event.inventory( + 449 15 : AgentSwitched, + 450 15 : ModelSwitched, + 451 7 : Moved, + 452 10 : Prompted, + 453 16 : PromptAdmitted, + 454 16 : ContextUpdated, + 455 11 : Synthetic, + 456 15 : Shell.Started, + 457 13 : Shell.Ended, + 458 14 : Step.Started, + 459 12 : Step.Ended, + 460 13 : Step.Failed, + 461 14 : Text.Started, + 462 12 : Text.Ended, + 463 20 : Tool.Input.Started, + 464 18 : Tool.Input.Ended, + 465 13 : Tool.Called, + 466 15 : Tool.Progress, + 467 14 : Tool.Success, + 468 13 : Tool.Failed, + 469 19 : Reasoning.Started, + 470 17 : Reasoning.Ended, + 471 9 : Retried, + 472 20 : Compaction.Started, + 473 18 : Compaction.Ended, + 474 20 : RevertEvent.Staged, + 475 21 : RevertEvent.Cleared, + 476 21 : RevertEvent.Committed, + 477 3 : ) + 478 : + 479 42 : export const Definitions = Event.inventory( + 480 15 : AgentSwitched, + 481 15 : ModelSwitched, + 482 7 : Moved, + 483 10 : Prompted, + 484 16 : PromptAdmitted, + 485 16 : ContextUpdated, + 486 11 : Synthetic, + 487 15 : Shell.Started, + 488 13 : Shell.Ended, + 489 14 : Step.Started, + 490 12 : Step.Ended, + 491 13 : Step.Failed, + 492 14 : Text.Started, + 493 12 : Text.Delta, + 494 12 : Text.Ended, + 495 19 : Reasoning.Started, + 496 17 : Reasoning.Delta, + 497 17 : Reasoning.Ended, + 498 20 : Tool.Input.Started, + 499 18 : Tool.Input.Delta, + 500 18 : Tool.Input.Ended, + 501 13 : Tool.Called, + 502 15 : Tool.Progress, + 503 14 : Tool.Success, + 504 13 : Tool.Failed, + 505 9 : Retried, + 506 20 : Compaction.Started, + 507 18 : Compaction.Delta, + 508 18 : Compaction.Ended, + 509 20 : RevertEvent.Staged, + 510 21 : RevertEvent.Cleared, + 511 21 : RevertEvent.Committed, + 512 3 : ) + 513 : + 514 74 : export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" }) + 515 35 : .pipe(Schema.toTaggedUnion("type")) + 516 49 : .annotate({ identifier: "SessionDurableEvent" }) + 517 : export type DurableEvent = typeof Durable.Type + 518 : + 519 98 : export const All = Schema.Union(Definitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type")) + 520 : export type Event = typeof All.Type + 521 : export type Type = Event["type"] ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Schema } from "effect" + 2 42 : import { descending } from "./identifier" + 3 35 : import { statics } from "./schema" + 4 : + 5 77 : export const SessionID = Schema.String.check(Schema.isStartsWith("ses")).pipe( + 6 27 : Schema.brand("SessionID"), + 7 22 : statics((schema) => { + 8 56 : const create = () => schema.make("ses_" + descending()) + 9 12 : return { + 10 11 : create, + 11 13 : descending: (id?: string) => (id === undefined ? create() : schema.make(id)), + 12 1 : } + 13 1 : }), + 14 2 : ) + 15 : export type SessionID = typeof SessionID.Type ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 48 : export * as SessionInput from "./session-input" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 34 : import { Prompt } from "./prompt" + 6 65 : import { DateTimeUtcFromMillis, NonNegativeInt } from "./schema" + 7 53 : import { SessionDelivery } from "./session-delivery" + 8 41 : import { SessionID } from "./session-id" + 9 51 : import { SessionMessage } from "./session-message" + 10 : + 11 49 : export const Delivery = SessionDelivery.Delivery + 12 : export type Delivery = SessionDelivery.Delivery + 13 : + 14 : export interface Admitted extends Schema.Schema.Type<typeof Admitted> {} + 15 40 : export const Admitted = Schema.Struct({ + 16 30 : admittedSeq: NonNegativeInt, + 17 24 : id: SessionMessage.ID, + 18 23 : sessionID: SessionID, + 19 17 : prompt: Prompt, + 20 21 : delivery: Delivery, + 21 37 : timeCreated: DateTimeUtcFromMillis, + 22 43 : promotedSeq: NonNegativeInt.pipe(optional), + 23 52 : }).annotate({ identifier: "SessionInput.Admitted" }) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 52 : export * as SessionMessage from "./session-message" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 54 : import { ProviderMetadata, ToolContent } from "./llm" + 6 32 : import { Model } from "./model" + 7 50 : import { FileAttachment, Prompt } from "./prompt" + 8 72 : import { DateTimeUtcFromMillis, RelativePath, statics } from "./schema" + 9 41 : import { SessionID } from "./session-id" + 10 41 : import { ascending } from "./identifier" + 11 : + 12 71 : export const ID = Schema.String.check(Schema.isStartsWith("msg_")).pipe( + 13 36 : Schema.brand("Session.Message.ID"), + 14 70 : statics((schema) => ({ create: () => schema.make("msg_" + ascending()) })), + 15 3 : ) + 16 : export type ID = typeof ID.Type + 17 : + 18 : export interface UnknownError extends Schema.Schema.Type<typeof UnknownError> {} + 19 44 : export const UnknownError = Schema.Struct({ + 20 34 : type: Schema.Literal("unknown"), + 21 23 : message: Schema.String, + 22 53 : }).annotate({ identifier: "Session.Error.Unknown" }) + 23 : + 24 15 : const Base = { + 25 9 : id: ID, + 26 72 : metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional), + 27 56 : time: Schema.Struct({ created: DateTimeUtcFromMillis }), + 28 2 : } + 29 : + 30 : export interface AgentSwitched extends Schema.Schema.Type<typeof AgentSwitched> {} + 31 48 : export const AgentSwitched = Schema.Struct({ + 32 7 : ...Base, + 33 41 : type: Schema.Literal("agent-switched"), + 34 21 : agent: Schema.String, + 35 61 : }).annotate({ identifier: "Session.Message.AgentSwitched" }) + 36 : + 37 : export interface ModelSwitched extends Schema.Schema.Type<typeof ModelSwitched> {} + 38 48 : export const ModelSwitched = Schema.Struct({ + 39 7 : ...Base, + 40 41 : type: Schema.Literal("model-switched"), + 41 17 : model: Model.Ref, + 42 61 : }).annotate({ identifier: "Session.Message.ModelSwitched" }) + 43 : + 44 : export interface User extends Schema.Schema.Type<typeof User> {} + 45 39 : export const User = Schema.Struct({ + 46 7 : ...Base, + 47 27 : text: Prompt.fields.text, + 48 29 : files: Prompt.fields.files, + 49 31 : agents: Prompt.fields.agents, + 50 29 : type: Schema.Literal("user"), + 51 52 : }).annotate({ identifier: "Session.Message.User" }) + 52 : + 53 : export interface Synthetic extends Schema.Schema.Type<typeof Synthetic> {} + 54 44 : export const Synthetic = Schema.Struct({ + 55 7 : ...Base, + 56 23 : sessionID: SessionID, + 57 22 : text: Schema.String, + 58 34 : type: Schema.Literal("synthetic"), + 59 57 : }).annotate({ identifier: "Session.Message.Synthetic" }) + 60 : + 61 : export interface System extends Schema.Schema.Type<typeof System> {} + 62 41 : export const System = Schema.Struct({ + 63 7 : ...Base, + 64 33 : type: Schema.Literal("system"), + 65 20 : text: Schema.String, + 66 54 : }).annotate({ identifier: "Session.Message.System" }) + 67 : + 68 : export interface Shell extends Schema.Schema.Type<typeof Shell> {} + 69 40 : export const Shell = Schema.Struct({ + 70 7 : ...Base, + 71 32 : type: Schema.Literal("shell"), + 72 24 : callID: Schema.String, + 73 25 : command: Schema.String, + 74 24 : output: Schema.String, + 75 25 : time: Schema.Struct({ + 76 35 : created: DateTimeUtcFromMillis, + 77 49 : completed: DateTimeUtcFromMillis.pipe(optional), + 78 3 : }), + 79 53 : }).annotate({ identifier: "Session.Message.Shell" }) + 80 : + 81 : export interface ToolStatePending extends Schema.Schema.Type<typeof ToolStatePending> {} + 82 48 : export const ToolStatePending = Schema.Struct({ + 83 36 : status: Schema.Literal("pending"), + 84 21 : input: Schema.String, + 85 65 : }).annotate({ identifier: "Session.Message.ToolState.Pending" }) + 86 : + 87 : export interface ToolStateRunning extends Schema.Schema.Type<typeof ToolStateRunning> {} + 88 48 : export const ToolStateRunning = Schema.Struct({ + 89 36 : status: Schema.Literal("running"), + 90 54 : input: Schema.Record(Schema.String, Schema.Unknown), + 91 59 : structured: Schema.Record(Schema.String, Schema.Unknown), + 92 40 : content: ToolContent.pipe(Schema.Array), + 93 65 : }).annotate({ identifier: "Session.Message.ToolState.Running" }) + 94 : + 95 : export interface ToolStateCompleted extends Schema.Schema.Type<typeof ToolStateCompleted> {} + 96 50 : export const ToolStateCompleted = Schema.Struct({ + 97 38 : status: Schema.Literal("completed"), + 98 54 : input: Schema.Record(Schema.String, Schema.Unknown), + 99 59 : attachments: FileAttachment.pipe(Schema.Array, optional), + 100 42 : content: ToolContent.pipe(Schema.Array), + 101 58 : outputPaths: Schema.Array(Schema.String).pipe(optional), + 102 59 : structured: Schema.Record(Schema.String, Schema.Unknown), + 103 38 : result: Schema.Unknown.pipe(optional), + 104 67 : }).annotate({ identifier: "Session.Message.ToolState.Completed" }) + 105 : + 106 : export interface ToolStateError extends Schema.Schema.Type<typeof ToolStateError> {} + 107 46 : export const ToolStateError = Schema.Struct({ + 108 34 : status: Schema.Literal("error"), + 109 54 : input: Schema.Record(Schema.String, Schema.Unknown), + 110 42 : content: ToolContent.pipe(Schema.Array), + 111 59 : structured: Schema.Record(Schema.String, Schema.Unknown), + 112 22 : error: UnknownError, + 113 38 : result: Schema.Unknown.pipe(optional), + 114 63 : }).annotate({ identifier: "Session.Message.ToolState.Error" }) + 115 : + 116 116 : export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe( + 117 30 : Schema.toTaggedUnion("status"), + 118 3 : ) + 119 : export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError + 120 : + 121 : export interface AssistantTool extends Schema.Schema.Type<typeof AssistantTool> {} + 122 45 : export const AssistantTool = Schema.Struct({ + 123 31 : type: Schema.Literal("tool"), + 124 20 : id: Schema.String, + 125 22 : name: Schema.String, + 126 29 : provider: Schema.Struct({ + 127 29 : executed: Schema.Boolean, + 128 46 : metadata: ProviderMetadata.pipe(optional), + 129 49 : resultMetadata: ProviderMetadata.pipe(optional), + 130 20 : }).pipe(optional), + 131 19 : state: ToolState, + 132 25 : time: Schema.Struct({ + 133 35 : created: DateTimeUtcFromMillis, + 134 46 : ran: DateTimeUtcFromMillis.pipe(optional), + 135 52 : completed: DateTimeUtcFromMillis.pipe(optional), + 136 46 : pruned: DateTimeUtcFromMillis.pipe(optional), + 137 3 : }), + 138 62 : }).annotate({ identifier: "Session.Message.Assistant.Tool" }) + 139 : + 140 : export interface AssistantText extends Schema.Schema.Type<typeof AssistantText> {} + 141 45 : export const AssistantText = Schema.Struct({ + 142 31 : type: Schema.Literal("text"), + 143 20 : id: Schema.String, + 144 20 : text: Schema.String, + 145 62 : }).annotate({ identifier: "Session.Message.Assistant.Text" }) + 146 : + 147 : export interface AssistantReasoning extends Schema.Schema.Type<typeof AssistantReasoning> {} + 148 50 : export const AssistantReasoning = Schema.Struct({ + 149 36 : type: Schema.Literal("reasoning"), + 150 20 : id: Schema.String, + 151 22 : text: Schema.String, + 152 52 : providerMetadata: ProviderMetadata.pipe(optional), + 153 25 : time: Schema.Struct({ + 154 35 : created: DateTimeUtcFromMillis, + 155 49 : completed: DateTimeUtcFromMillis.pipe(optional), + 156 18 : }).pipe(optional), + 157 67 : }).annotate({ identifier: "Session.Message.Assistant.Reasoning" }) + 158 : + 159 101 : export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe( + 160 28 : Schema.toTaggedUnion("type"), + 161 3 : ) + 162 : export type AssistantContent = AssistantText | AssistantReasoning | AssistantTool + 163 : + 164 : export interface Assistant extends Schema.Schema.Type<typeof Assistant> {} + 165 44 : export const Assistant = Schema.Struct({ + 166 7 : ...Base, + 167 36 : type: Schema.Literal("assistant"), + 168 23 : agent: Schema.String, + 169 19 : model: Model.Ref, + 170 47 : content: AssistantContent.pipe(Schema.Array), + 171 29 : snapshot: Schema.Struct({ + 172 40 : start: Schema.String.pipe(optional), + 173 38 : end: Schema.String.pipe(optional), + 174 50 : files: Schema.Array(RelativePath).pipe(optional), + 175 20 : }).pipe(optional), + 176 39 : finish: Schema.String.pipe(optional), + 177 37 : cost: Schema.Finite.pipe(optional), + 178 27 : tokens: Schema.Struct({ + 179 25 : input: Schema.Finite, + 180 26 : output: Schema.Finite, + 181 29 : reasoning: Schema.Finite, + 182 69 : cache: Schema.Struct({ read: Schema.Finite, write: Schema.Finite }), + 183 20 : }).pipe(optional), + 184 37 : error: UnknownError.pipe(optional), + 185 25 : time: Schema.Struct({ + 186 35 : created: DateTimeUtcFromMillis, + 187 49 : completed: DateTimeUtcFromMillis.pipe(optional), + 188 3 : }), + 189 57 : }).annotate({ identifier: "Session.Message.Assistant" }) + 190 : + 191 : export interface Compaction extends Schema.Schema.Type<typeof Compaction> {} + 192 42 : export const Compaction = Schema.Struct({ + 193 37 : type: Schema.Literal("compaction"), + 194 46 : reason: Schema.Literals(["auto", "manual"]), + 195 25 : summary: Schema.String, + 196 27 : recent: Schema.String, + 197 5 : ...Base, + 198 58 : }).annotate({ identifier: "Session.Message.Compaction" }) + 199 : + 200 38 : export const Message = Schema.Union([ + 201 16 : AgentSwitched, + 202 16 : ModelSwitched, + 203 7 : User, + 204 12 : Synthetic, + 205 9 : System, + 206 8 : Shell, + 207 12 : Assistant, + 208 11 : Compaction, + 209 2 : ]) + 210 35 : .pipe(Schema.toTaggedUnion("type")) + 211 44 : .annotate({ identifier: "Session.Message" }) + 212 : export type Message = AgentSwitched | ModelSwitched | User | Synthetic | System | Shell | Assistant | Compaction + 213 : export type Type = Message["type"] ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 46 : export * as SessionTodo from "./session-todo" + 2 : + 3 32 : import { Schema } from "effect" + 4 44 : import { define, inventory } from "./event" + 5 41 : import { SessionID } from "./session-id" + 6 : + 7 36 : export const Info = Schema.Struct({ + 8 84 : content: Schema.String.annotate({ description: "Brief description of the task" }), + 9 36 : status: Schema.String.annotate({ + 10 87 : description: "Current status of the task: pending, in_progress, completed, cancelled", + 11 5 : }), + 12 38 : priority: Schema.String.annotate({ + 13 62 : description: "Priority level of the task: high, medium, low", + 14 3 : }), + 15 36 : }).annotate({ identifier: "Todo" }) + 16 : export interface Info extends Schema.Schema.Type<typeof Info> {} + 17 : + 18 25 : const Updated = define({ + 19 23 : type: "todo.updated", + 20 13 : schema: { + 21 25 : sessionID: SessionID, + 22 27 : todos: Schema.Array(Info), + 23 2 : }, + 24 3 : }) + 25 65 : export const Event = { Updated, Definitions: inventory(Updated) } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+
+ 1 28 : export * from "./v1/session"
+
+ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 37 : export * as Session from "./session" + 2 : + 3 32 : import { Schema } from "effect" + 4 32 : import { Agent } from "./agent" + 5 38 : import { Location } from "./location" + 6 32 : import { Model } from "./model" + 7 36 : import { Project } from "./project" + 8 73 : import { DateTimeUtcFromMillis, optional, RelativePath } from "./schema" + 9 47 : import { SessionEvent } from "./session-event" + 10 41 : import { SessionID } from "./session-id" + 11 34 : import { Revert } from "./revert" + 12 : + 13 28 : export const ID = SessionID + 14 : export type ID = SessionID + 15 : + 16 34 : export const Event = SessionEvent + 17 : + 18 : export interface Info extends Schema.Schema.Type<typeof Info> {} + 19 36 : export const Info = Schema.Struct({ + 20 9 : id: ID, + 21 30 : parentID: ID.pipe(optional), + 22 24 : projectID: Project.ID, + 23 33 : agent: Agent.ID.pipe(optional), + 24 34 : model: Model.Ref.pipe(optional), + 25 22 : cost: Schema.Finite, + 26 27 : tokens: Schema.Struct({ + 27 25 : input: Schema.Finite, + 28 26 : output: Schema.Finite, + 29 29 : reasoning: Schema.Finite, + 30 28 : cache: Schema.Struct({ + 31 26 : read: Schema.Finite, + 32 24 : write: Schema.Finite, + 33 4 : }), + 34 5 : }), + 35 25 : time: Schema.Struct({ + 36 35 : created: DateTimeUtcFromMillis, + 37 35 : updated: DateTimeUtcFromMillis, + 38 48 : archived: DateTimeUtcFromMillis.pipe(optional), + 39 5 : }), + 40 23 : title: Schema.String, + 41 25 : location: Location.Ref, + 42 39 : subpath: RelativePath.pipe(optional), + 43 36 : revert: Revert.State.pipe(optional), + 44 46 : }).annotate({ identifier: "SessionV2.Info" }) + 45 : + 46 42 : export const ListAnchor = Schema.Struct({ + 47 9 : id: ID, + 48 22 : time: Schema.Finite, + 49 49 : direction: Schema.Literals(["previous", "next"]), + 50 49 : }).annotate({ identifier: "Session.ListAnchor" }) + 51 : export interface ListAnchor extends Schema.Schema.Type<typeof ListAnchor> {} ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 33 : export * as Skill from "./skill" + 2 : + 3 32 : import { Schema } from "effect" + 4 36 : import { optional } from "./schema" + 5 40 : import { AbsolutePath } from "./schema" + 6 : + 7 : export interface DirectorySource extends Schema.Schema.Type<typeof DirectorySource> {} + 8 47 : export const DirectorySource = Schema.Struct({ + 9 36 : type: Schema.Literal("directory"), + 10 19 : path: AbsolutePath, + 11 55 : }).annotate({ identifier: "SkillV2.DirectorySource" }) + 12 : + 13 : export interface UrlSource extends Schema.Schema.Type<typeof UrlSource> {} + 14 41 : export const UrlSource = Schema.Struct({ + 15 30 : type: Schema.Literal("url"), + 16 19 : url: Schema.String, + 17 49 : }).annotate({ identifier: "SkillV2.UrlSource" }) + 18 : + 19 : export interface Info extends Schema.Schema.Type<typeof Info> {} + 20 36 : export const Info = Schema.Struct({ + 21 22 : name: Schema.String, + 22 44 : description: Schema.String.pipe(optional), + 23 39 : slash: Schema.Boolean.pipe(optional), + 24 25 : location: AbsolutePath, + 25 23 : content: Schema.String, + 26 44 : }).annotate({ identifier: "SkillV2.Info" }) + 27 : + 28 : export interface EmbeddedSource extends Schema.Schema.Type<typeof EmbeddedSource> {} + 29 46 : export const EmbeddedSource = Schema.Struct({ + 30 35 : type: Schema.Literal("embedded"), + 31 32 : skill: Schema.suspend(() => Info), + 32 54 : }).annotate({ identifier: "SkillV2.EmbeddedSource" }) + 33 : + 34 : export type Source = DirectorySource | UrlSource | EmbeddedSource + 35 35 : export const Source = Object.assign( + 36 64 : Schema.Union([DirectorySource, UrlSource, EmbeddedSource]).pipe( + 37 30 : Schema.toTaggedUnion("type"), + 38 49 : Schema.annotate({ identifier: "SkillV2.Source" }), + 39 3 : ), + 40 3 : { + 41 22 : equals: (a: Source, b: Source) => { + 42 43 : if (a.type !== b.type) return false + 43 82 : if (a.type === "directory" && b.type === "directory") return a.path === b.path + 44 0 : if (a.type === "url" && b.type === "url") return a.url === b.url + 45 0 : if (a.type === "embedded" && b.type === "embedded") return a.skill.name === b.skill.name + 46 3 : return false + 47 : }, + 48 16 : key: (source: Source) => + 49 29 : source.type === "directory" + 50 28 : ? `directory:${source.path}` + 51 23 : : source.type === "url" + 52 21 : ? `url:${source.url}` + 53 32 : : `embedded:${source.skill.name}`, + 54 : }, + 55 2 : ) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| permission.ts | +
+ |
+ 100.0 % | +44 | +44 | +|
| session.ts | +
+ |
+ 100.0 % | +563 | +563 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| permission.ts | +
+ |
+ 100.0 % | +44 | +44 | +|
| session.ts | +
+ |
+ 100.0 % | +563 | +563 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | ||
| File |
+ Line Coverage |
+ ||||
| Rate | +Total | +Hit | +|||
| permission.ts | +
+ |
+ 100.0 % | +44 | +44 | +|
| session.ts | +
+ |
+ 100.0 % | +563 | +563 | +|
| Note: 'Function Coverage' columns elided as function owner is not identified. | +|||||
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 45 : export * as PermissionV1 from "./permission" + 2 : + 3 32 : import { Schema } from "effect" + 4 45 : import { define, inventory } from "../event" + 5 42 : import { ascending } from "../identifier" + 6 37 : import { Project } from "../project" + 7 36 : import { statics } from "../schema" + 8 42 : import { SessionID } from "../session-id" + 9 : + 10 70 : export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe( + 11 30 : Schema.brand("PermissionID"), + 12 35 : statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "per_" + ascending()) })), + 13 3 : ) + 14 : export type ID = typeof ID.Type + 15 : + 16 109 : export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionAction" }) + 17 : export type Action = typeof Action.Type + 18 : + 19 116 : export const Rule = Schema.Struct({ permission: Schema.String, pattern: Schema.String, action: Action }).annotate({ + 20 29 : identifier: "PermissionRule", + 21 3 : }) + 22 : export type Rule = typeof Rule.Type + 23 : + 24 88 : export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionRuleset" }) + 25 : export type Ruleset = typeof Ruleset.Type + 26 : + 27 39 : export const Request = Schema.Struct({ + 28 9 : id: ID, + 29 23 : sessionID: SessionID, + 30 28 : permission: Schema.String, + 31 40 : patterns: Schema.Array(Schema.String), + 32 57 : metadata: Schema.Record(Schema.String, Schema.Unknown), + 33 38 : always: Schema.Array(Schema.String), + 34 90 : tool: Schema.optional(Schema.Struct({ messageID: Schema.String, callID: Schema.String })), + 35 49 : }).annotate({ identifier: "PermissionRequest" }) + 36 : export type Request = typeof Request.Type + 37 : + 38 67 : export const Reply = Schema.Literals(["once", "always", "reject"]) + 39 : export type Reply = typeof Reply.Type + 40 : + 41 109 : export const ReplyBody = Schema.Struct({ reply: Reply, message: Schema.optional(Schema.String) }).annotate({ + 42 34 : identifier: "PermissionReplyBody", + 43 3 : }) + 44 : export type ReplyBody = typeof ReplyBody.Type + 45 : + 46 115 : export const Approval = Schema.Struct({ projectID: Project.ID, patterns: Schema.Array(Schema.String) }).annotate({ + 47 33 : identifier: "PermissionApproval", + 48 3 : }) + 49 : export type Approval = typeof Approval.Type + 50 : + 51 115 : export const AskInput = Schema.Struct({ ...Request.fields, id: Schema.optional(ID), ruleset: Ruleset }).annotate({ + 52 33 : identifier: "PermissionAskInput", + 53 3 : }) + 54 : export type AskInput = typeof AskInput.Type + 55 : + 56 91 : export const ReplyInput = Schema.Struct({ requestID: ID, ...ReplyBody.fields }).annotate({ + 57 35 : identifier: "PermissionReplyInput", + 58 3 : }) + 59 : export type ReplyInput = typeof ReplyInput.Type + 60 : + 61 75 : const Asked = define({ type: "permission.asked", schema: Request.fields }) + 62 25 : const Replied = define({ + 63 29 : type: "permission.replied", + 64 62 : schema: { sessionID: SessionID, requestID: ID, reply: Reply }, + 65 3 : }) + 66 79 : export const Event = { Asked, Replied, Definitions: inventory(Asked, Replied) } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 1 : export * as SessionV1 from "./session" + 2 : + 3 40 : import { Effect, Schema, Types } from "effect" + 4 45 : import { define, inventory } from "../event" + 5 40 : import { FileDiff } from "../file-diff" + 6 37 : import { Project } from "../project" + 7 39 : import { Provider } from "../provider" + 8 33 : import { Model } from "../model" + 9 62 : import { NonNegativeInt, optional, statics } from "../schema" + 10 42 : import { ascending } from "../identifier" + 11 42 : import { SessionID } from "../session-id" + 12 46 : import { WorkspaceID } from "../workspace-id" + 13 44 : import { PermissionV1 } from "./permission" + 14 : + 15 72 : const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)) + 16 : + 17 77 : export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe( + 18 27 : Schema.brand("MessageID"), + 19 81 : statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "msg_" + ascending()) })), + 20 3 : ) + 21 : export type MessageID = typeof MessageID.Type + 22 : + 23 74 : export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe( + 24 24 : Schema.brand("PartID"), + 25 35 : statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "prt_" + ascending()) })), + 26 3 : ) + 27 : export type PartID = typeof PartID.Type + 28 : + 29 38 : const namedError = <Name extends string, Fields extends Schema.Struct.Fields>(name: Name, fields: Fields) => { + 30 104 : const schema = Schema.Struct({ name: Schema.Literal(name), data: Schema.Struct(fields) }).annotate({ + 31 18 : identifier: name, + 32 5 : }) + 33 49 : return { Schema: schema, EffectSchema: schema } + 34 : } + 35 : + 36 76 : export const OutputLengthError = namedError("MessageOutputLengthError", {}) + 37 : + 38 59 : export const AuthError = namedError("ProviderAuthError", { + 39 28 : providerID: Schema.String, + 40 23 : message: Schema.String, + 41 3 : }) + 42 : + 43 90 : export const AbortedError = namedError("MessageAbortedError", { message: Schema.String }) + 44 75 : export const StructuredOutputError = namedError("StructuredOutputError", { + 45 25 : message: Schema.String, + 46 24 : retries: NonNegativeInt, + 47 3 : }) + 48 49 : export const APIError = namedError("APIError", { + 49 25 : message: Schema.String, + 50 46 : statusCode: Schema.optional(NonNegativeInt), + 51 30 : isRetryable: Schema.Boolean, + 52 80 : responseHeaders: Schema.optional(Schema.Record(Schema.String, Schema.String)), + 53 47 : responseBody: Schema.optional(Schema.String), + 54 71 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), + 55 3 : }) + 56 : export type APIError = Schema.Schema.Type<typeof APIError.Schema> + 57 73 : export const ContextOverflowError = namedError("ContextOverflowError", { + 58 25 : message: Schema.String, + 59 45 : responseBody: Schema.optional(Schema.String), + 60 3 : }) + 61 69 : export const ContentFilterError = namedError("ContentFilterError", { + 62 23 : message: Schema.String, + 63 3 : }) + 64 : + 65 73 : export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputFormatText")({ + 66 29 : type: Schema.Literal("text"), + 67 5 : }) {} + 68 : + 69 85 : export class OutputFormatJsonSchema extends Schema.Class<OutputFormatJsonSchema>("OutputFormatJsonSchema")({ + 70 38 : type: Schema.Literal("json_schema"), + 71 90 : schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }), + 72 96 : retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))), + 73 5 : }) {} + 74 : + 75 90 : export const Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({ + 76 24 : discriminator: "type", + 77 27 : identifier: "OutputFormat", + 78 3 : }) + 79 : export type OutputFormat = Schema.Schema.Type<typeof Format> + 80 : + 81 19 : const partBase = { + 82 13 : id: PartID, + 83 23 : sessionID: SessionID, + 84 21 : messageID: MessageID, + 85 2 : } + 86 : + 87 47 : export const SnapshotPart = Schema.Struct({ + 88 11 : ...partBase, + 89 35 : type: Schema.Literal("snapshot"), + 90 24 : snapshot: Schema.String, + 91 44 : }).annotate({ identifier: "SnapshotPart" }) + 92 : export type SnapshotPart = Types.DeepMutable<Schema.Schema.Type<typeof SnapshotPart>> + 93 : + 94 44 : export const PatchPart = Schema.Struct({ + 95 11 : ...partBase, + 96 32 : type: Schema.Literal("patch"), + 97 22 : hash: Schema.String, + 98 35 : files: Schema.Array(Schema.String), + 99 41 : }).annotate({ identifier: "PatchPart" }) + 100 : export type PatchPart = Types.DeepMutable<Schema.Schema.Type<typeof PatchPart>> + 101 : + 102 43 : export const TextPart = Schema.Struct({ + 103 11 : ...partBase, + 104 31 : type: Schema.Literal("text"), + 105 22 : text: Schema.String, + 106 45 : synthetic: Schema.optional(Schema.Boolean), + 107 43 : ignored: Schema.optional(Schema.Boolean), + 108 22 : time: Schema.optional( + 109 19 : Schema.Struct({ + 110 26 : start: NonNegativeInt, + 111 38 : end: Schema.optional(NonNegativeInt), + 112 2 : }), + 113 4 : ), + 114 68 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), + 115 40 : }).annotate({ identifier: "TextPart" }) + 116 : export type TextPart = Types.DeepMutable<Schema.Schema.Type<typeof TextPart>> + 117 : + 118 48 : export const ReasoningPart = Schema.Struct({ + 119 11 : ...partBase, + 120 36 : type: Schema.Literal("reasoning"), + 121 22 : text: Schema.String, + 122 70 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), + 123 25 : time: Schema.Struct({ + 124 26 : start: NonNegativeInt, + 125 38 : end: Schema.optional(NonNegativeInt), + 126 3 : }), + 127 45 : }).annotate({ identifier: "ReasoningPart" }) + 128 : export type ReasoningPart = Types.DeepMutable<Schema.Schema.Type<typeof ReasoningPart>> + 129 : + 130 29 : const filePartSourceBase = { + 131 25 : text: Schema.Struct({ + 132 25 : value: Schema.String, + 133 25 : start: Schema.Finite, + 134 20 : end: Schema.Finite, + 135 50 : }).annotate({ identifier: "FilePartSourceText" }), + 136 2 : } + 137 : + 138 37 : export const Range = Schema.Struct({ + 139 76 : start: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }), + 140 72 : end: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }), + 141 37 : }).annotate({ identifier: "Range" }) + 142 : export type Range = typeof Range.Type + 143 : + 144 45 : export const FileSource = Schema.Struct({ + 145 21 : ...filePartSourceBase, + 146 31 : type: Schema.Literal("file"), + 147 20 : path: Schema.String, + 148 42 : }).annotate({ identifier: "FileSource" }) + 149 : + 150 47 : export const SymbolSource = Schema.Struct({ + 151 21 : ...filePartSourceBase, + 152 33 : type: Schema.Literal("symbol"), + 153 22 : path: Schema.String, + 154 15 : range: Range, + 155 22 : name: Schema.String, + 156 21 : kind: NonNegativeInt, + 157 44 : }).annotate({ identifier: "SymbolSource" }) + 158 : + 159 49 : export const ResourceSource = Schema.Struct({ + 160 21 : ...filePartSourceBase, + 161 35 : type: Schema.Literal("resource"), + 162 28 : clientName: Schema.String, + 163 19 : uri: Schema.String, + 164 46 : }).annotate({ identifier: "ResourceSource" }) + 165 : + 166 98 : export const FilePartSource = Schema.Union([FileSource, SymbolSource, ResourceSource]).annotate({ + 167 24 : discriminator: "type", + 168 29 : identifier: "FilePartSource", + 169 3 : }) + 170 : + 171 43 : export const FilePart = Schema.Struct({ + 172 11 : ...partBase, + 173 31 : type: Schema.Literal("file"), + 174 22 : mime: Schema.String, + 175 43 : filename: Schema.optional(Schema.String), + 176 21 : url: Schema.String, + 177 40 : source: Schema.optional(FilePartSource), + 178 40 : }).annotate({ identifier: "FilePart" }) + 179 : export type FilePart = Types.DeepMutable<Schema.Schema.Type<typeof FilePart>> + 180 : + 181 44 : export const AgentPart = Schema.Struct({ + 182 11 : ...partBase, + 183 32 : type: Schema.Literal("agent"), + 184 22 : name: Schema.String, + 185 24 : source: Schema.optional( + 186 19 : Schema.Struct({ + 187 25 : value: Schema.String, + 188 26 : start: NonNegativeInt, + 189 21 : end: NonNegativeInt, + 190 2 : }), + 191 2 : ), + 192 41 : }).annotate({ identifier: "AgentPart" }) + 193 : export type AgentPart = Types.DeepMutable<Schema.Schema.Type<typeof AgentPart>> + 194 : + 195 49 : export const CompactionPart = Schema.Struct({ + 196 11 : ...partBase, + 197 37 : type: Schema.Literal("compaction"), + 198 23 : auto: Schema.Boolean, + 199 44 : overflow: Schema.optional(Schema.Boolean), + 200 42 : tail_start_id: Schema.optional(MessageID), + 201 46 : }).annotate({ identifier: "CompactionPart" }) + 202 : export type CompactionPart = Types.DeepMutable<Schema.Schema.Type<typeof CompactionPart>> + 203 : + 204 46 : export const SubtaskPart = Schema.Struct({ + 205 11 : ...partBase, + 206 34 : type: Schema.Literal("subtask"), + 207 24 : prompt: Schema.String, + 208 29 : description: Schema.String, + 209 23 : agent: Schema.String, + 210 23 : model: Schema.optional( + 211 19 : Schema.Struct({ + 212 28 : providerID: Provider.ID, + 213 19 : modelID: Model.ID, + 214 2 : }), + 215 4 : ), + 216 40 : command: Schema.optional(Schema.String), + 217 43 : }).annotate({ identifier: "SubtaskPart" }) + 218 : export type SubtaskPart = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPart>> + 219 : + 220 44 : export const RetryPart = Schema.Struct({ + 221 11 : ...partBase, + 222 32 : type: Schema.Literal("retry"), + 223 26 : attempt: NonNegativeInt, + 224 31 : error: APIError.EffectSchema, + 225 25 : time: Schema.Struct({ + 226 25 : created: NonNegativeInt, + 227 3 : }), + 228 41 : }).annotate({ identifier: "RetryPart" }) + 229 : export type RetryPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof RetryPart>>, "error"> & { + 230 : error: APIError + 231 : } + 232 : + 233 48 : export const StepStartPart = Schema.Struct({ + 234 11 : ...partBase, + 235 37 : type: Schema.Literal("step-start"), + 236 41 : snapshot: Schema.optional(Schema.String), + 237 45 : }).annotate({ identifier: "StepStartPart" }) + 238 : export type StepStartPart = Types.DeepMutable<Schema.Schema.Type<typeof StepStartPart>> + 239 : + 240 49 : export const StepFinishPart = Schema.Struct({ + 241 11 : ...partBase, + 242 38 : type: Schema.Literal("step-finish"), + 243 24 : reason: Schema.String, + 244 43 : snapshot: Schema.optional(Schema.String), + 245 22 : cost: Schema.Finite, + 246 27 : tokens: Schema.Struct({ + 247 42 : total: Schema.optional(Schema.Finite), + 248 25 : input: Schema.Finite, + 249 26 : output: Schema.Finite, + 250 29 : reasoning: Schema.Finite, + 251 28 : cache: Schema.Struct({ + 252 26 : read: Schema.Finite, + 253 24 : write: Schema.Finite, + 254 4 : }), + 255 3 : }), + 256 46 : }).annotate({ identifier: "StepFinishPart" }) + 257 : export type StepFinishPart = Types.DeepMutable<Schema.Schema.Type<typeof StepFinishPart>> + 258 : + 259 48 : export const ToolStatePending = Schema.Struct({ + 260 36 : status: Schema.Literal("pending"), + 261 50 : input: Schema.Record(Schema.String, Schema.Any), + 262 19 : raw: Schema.String, + 263 48 : }).annotate({ identifier: "ToolStatePending" }) + 264 : export type ToolStatePending = Types.DeepMutable<Schema.Schema.Type<typeof ToolStatePending>> + 265 : + 266 48 : export const ToolStateRunning = Schema.Struct({ + 267 36 : status: Schema.Literal("running"), + 268 50 : input: Schema.Record(Schema.String, Schema.Any), + 269 40 : title: Schema.optional(Schema.String), + 270 70 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), + 271 25 : time: Schema.Struct({ + 272 23 : start: NonNegativeInt, + 273 3 : }), + 274 48 : }).annotate({ identifier: "ToolStateRunning" }) + 275 : export type ToolStateRunning = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateRunning>> + 276 : + 277 50 : export const ToolStateCompleted = Schema.Struct({ + 278 38 : status: Schema.Literal("completed"), + 279 50 : input: Schema.Record(Schema.String, Schema.Any), + 280 24 : output: Schema.String, + 281 23 : title: Schema.String, + 282 53 : metadata: Schema.Record(Schema.String, Schema.Any), + 283 25 : time: Schema.Struct({ + 284 26 : start: NonNegativeInt, + 285 24 : end: NonNegativeInt, + 286 44 : compacted: Schema.optional(NonNegativeInt), + 287 5 : }), + 288 53 : attachments: Schema.optional(Schema.Array(FilePart)), + 289 50 : }).annotate({ identifier: "ToolStateCompleted" }) + 290 : export type ToolStateCompleted = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateCompleted>> + 291 : + 292 46 : export const ToolStateError = Schema.Struct({ + 293 34 : status: Schema.Literal("error"), + 294 50 : input: Schema.Record(Schema.String, Schema.Any), + 295 23 : error: Schema.String, + 296 70 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), + 297 25 : time: Schema.Struct({ + 298 26 : start: NonNegativeInt, + 299 21 : end: NonNegativeInt, + 300 3 : }), + 301 46 : }).annotate({ identifier: "ToolStateError" }) + 302 : export type ToolStateError = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateError>> + 303 : + 304 40 : export const ToolState = Schema.Union([ + 305 19 : ToolStatePending, + 306 19 : ToolStateRunning, + 307 21 : ToolStateCompleted, + 308 15 : ToolStateError, + 309 14 : ]).annotate({ + 310 26 : discriminator: "status", + 311 24 : identifier: "ToolState", + 312 3 : }) + 313 : export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError + 314 : + 315 43 : export const ToolPart = Schema.Struct({ + 316 11 : ...partBase, + 317 31 : type: Schema.Literal("tool"), + 318 24 : callID: Schema.String, + 319 22 : tool: Schema.String, + 320 19 : state: ToolState, + 321 68 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), + 322 40 : }).annotate({ identifier: "ToolPart" }) + 323 : export type ToolPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof ToolPart>>, "state"> & { + 324 : state: ToolState + 325 : } + 326 : + 327 22 : const messageBase = { + 328 16 : id: MessageID, + 329 30 : sessionID: partBase.sessionID, + 330 2 : } + 331 : + 332 39 : export const User = Schema.Struct({ + 333 14 : ...messageBase, + 334 31 : role: Schema.Literal("user"), + 335 25 : time: Schema.Struct({ + 336 20 : created: Timestamp, + 337 5 : }), + 338 34 : format: Schema.optional(Format), + 339 25 : summary: Schema.optional( + 340 19 : Schema.Struct({ + 341 42 : title: Schema.optional(Schema.String), + 342 41 : body: Schema.optional(Schema.String), + 343 36 : diffs: Schema.Array(FileDiff.Info), + 344 2 : }), + 345 4 : ), + 346 23 : agent: Schema.String, + 347 26 : model: Schema.Struct({ + 348 28 : providerID: Provider.ID, + 349 22 : modelID: Model.ID, + 350 41 : variant: Schema.optional(Schema.String), + 351 5 : }), + 352 41 : system: Schema.optional(Schema.String), + 353 69 : tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), + 354 43 : }).annotate({ identifier: "UserMessage" }) + 355 : export type User = Types.DeepMutable<Schema.Schema.Type<typeof User>> + 356 : + 357 35 : export const Part = Schema.Union([ + 358 11 : TextPart, + 359 14 : SubtaskPart, + 360 16 : ReasoningPart, + 361 11 : FilePart, + 362 11 : ToolPart, + 363 16 : StepStartPart, + 364 17 : StepFinishPart, + 365 15 : SnapshotPart, + 366 12 : PatchPart, + 367 12 : AgentPart, + 368 12 : RetryPart, + 369 15 : CompactionPart, + 370 59 : ]).annotate({ discriminator: "type", identifier: "Part" }) + 371 : export type Part = + 372 : | TextPart + 373 : | SubtaskPart + 374 : | ReasoningPart + 375 : | FilePart + 376 : | ToolPart + 377 : | StepStartPart + 378 : | StepFinishPart + 379 : | SnapshotPart + 380 : | PatchPart + 381 : | AgentPart + 382 : | RetryPart + 383 : | CompactionPart + 384 : + 385 44 : const AssistantErrorSchema = Schema.Union([ + 386 25 : AuthError.EffectSchema, + 387 107 : namedError("UnknownError", { message: Schema.String, ref: Schema.optional(Schema.String) }).EffectSchema, + 388 33 : OutputLengthError.EffectSchema, + 389 28 : AbortedError.EffectSchema, + 390 37 : StructuredOutputError.EffectSchema, + 391 36 : ContextOverflowError.EffectSchema, + 392 34 : ContentFilterError.EffectSchema, + 393 22 : APIError.EffectSchema, + 394 39 : ]).annotate({ discriminator: "name" }) + 395 : type AssistantError = Schema.Schema.Type<typeof AssistantErrorSchema> + 396 : + 397 45 : export const TextPartInput = Schema.Struct({ + 398 30 : id: Schema.optional(PartID), + 399 31 : type: Schema.Literal("text"), + 400 22 : text: Schema.String, + 401 45 : synthetic: Schema.optional(Schema.Boolean), + 402 43 : ignored: Schema.optional(Schema.Boolean), + 403 22 : time: Schema.optional( + 404 19 : Schema.Struct({ + 405 26 : start: NonNegativeInt, + 406 38 : end: Schema.optional(NonNegativeInt), + 407 2 : }), + 408 4 : ), + 409 68 : metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), + 410 45 : }).annotate({ identifier: "TextPartInput" }) + 411 : export type TextPartInput = Types.DeepMutable<Schema.Schema.Type<typeof TextPartInput>> + 412 : + 413 45 : export const FilePartInput = Schema.Struct({ + 414 30 : id: Schema.optional(PartID), + 415 31 : type: Schema.Literal("file"), + 416 22 : mime: Schema.String, + 417 43 : filename: Schema.optional(Schema.String), + 418 21 : url: Schema.String, + 419 40 : source: Schema.optional(FilePartSource), + 420 45 : }).annotate({ identifier: "FilePartInput" }) + 421 : export type FilePartInput = Types.DeepMutable<Schema.Schema.Type<typeof FilePartInput>> + 422 : + 423 46 : export const AgentPartInput = Schema.Struct({ + 424 30 : id: Schema.optional(PartID), + 425 32 : type: Schema.Literal("agent"), + 426 22 : name: Schema.String, + 427 24 : source: Schema.optional( + 428 19 : Schema.Struct({ + 429 25 : value: Schema.String, + 430 26 : start: NonNegativeInt, + 431 21 : end: NonNegativeInt, + 432 2 : }), + 433 2 : ), + 434 46 : }).annotate({ identifier: "AgentPartInput" }) + 435 : export type AgentPartInput = Types.DeepMutable<Schema.Schema.Type<typeof AgentPartInput>> + 436 : + 437 48 : export const SubtaskPartInput = Schema.Struct({ + 438 30 : id: Schema.optional(PartID), + 439 34 : type: Schema.Literal("subtask"), + 440 24 : prompt: Schema.String, + 441 29 : description: Schema.String, + 442 23 : agent: Schema.String, + 443 23 : model: Schema.optional( + 444 19 : Schema.Struct({ + 445 28 : providerID: Provider.ID, + 446 19 : modelID: Model.ID, + 447 2 : }), + 448 4 : ), + 449 40 : command: Schema.optional(Schema.String), + 450 48 : }).annotate({ identifier: "SubtaskPartInput" }) + 451 : export type SubtaskPartInput = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPartInput>> + 452 : + 453 44 : export const Assistant = Schema.Struct({ + 454 14 : ...messageBase, + 455 36 : role: Schema.Literal("assistant"), + 456 25 : time: Schema.Struct({ + 457 28 : created: NonNegativeInt, + 458 44 : completed: Schema.optional(NonNegativeInt), + 459 5 : }), + 460 47 : error: Schema.optional(AssistantErrorSchema), + 461 22 : parentID: MessageID, + 462 20 : modelID: Model.ID, + 463 26 : providerID: Provider.ID, + 464 22 : mode: Schema.String, + 465 23 : agent: Schema.String, + 466 25 : path: Schema.Struct({ + 467 23 : cwd: Schema.String, + 468 21 : root: Schema.String, + 469 5 : }), + 470 43 : summary: Schema.optional(Schema.Boolean), + 471 22 : cost: Schema.Finite, + 472 27 : tokens: Schema.Struct({ + 473 42 : total: Schema.optional(Schema.Finite), + 474 25 : input: Schema.Finite, + 475 26 : output: Schema.Finite, + 476 29 : reasoning: Schema.Finite, + 477 28 : cache: Schema.Struct({ + 478 26 : read: Schema.Finite, + 479 24 : write: Schema.Finite, + 480 4 : }), + 481 5 : }), + 482 42 : structured: Schema.optional(Schema.Any), + 483 42 : variant: Schema.optional(Schema.String), + 484 39 : finish: Schema.optional(Schema.String), + 485 48 : }).annotate({ identifier: "AssistantMessage" }) + 486 : export type Assistant = Omit<Types.DeepMutable<Schema.Schema.Type<typeof Assistant>>, "error"> & { + 487 : error?: AssistantError + 488 : } + 489 : + 490 111 : export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" }) + 491 : export type Info = User | Assistant + 492 : + 493 41 : export const WithParts = Schema.Struct({ + 494 13 : info: Info, + 495 26 : parts: Schema.Array(Part), + 496 3 : }) + 497 : export type WithParts = { + 498 : info: Info + 499 : parts: Part[] + 500 : } + 501 : + 502 18 : const options = { + 503 14 : durable: { + 504 27 : aggregate: "sessionID", + 505 12 : version: 1, + 506 2 : }, + 507 2 : } as const + 508 : + 509 39 : const SessionSummary = Schema.Struct({ + 510 27 : additions: Schema.Finite, + 511 27 : deletions: Schema.Finite, + 512 23 : files: Schema.Finite, + 513 45 : diffs: optional(Schema.Array(FileDiff.Info)), + 514 3 : }) + 515 : + 516 38 : const SessionTokens = Schema.Struct({ + 517 23 : input: Schema.Finite, + 518 24 : output: Schema.Finite, + 519 27 : reasoning: Schema.Finite, + 520 26 : cache: Schema.Struct({ + 521 24 : read: Schema.Finite, + 522 22 : write: Schema.Finite, + 523 3 : }), + 524 3 : }) + 525 : + 526 37 : const SessionShare = Schema.Struct({ + 527 19 : url: Schema.String, + 528 3 : }) + 529 : + 530 38 : const SessionRevert = Schema.Struct({ + 531 23 : messageID: MessageID, + 532 27 : partID: optional(PartID), + 533 36 : snapshot: optional(Schema.String), + 534 30 : diff: optional(Schema.String), + 535 3 : }) + 536 : + 537 37 : const SessionModel = Schema.Struct({ + 538 15 : id: Model.ID, + 539 26 : providerID: Provider.ID, + 540 33 : variant: optional(Schema.String), + 541 3 : }) + 542 : + 543 43 : export const SessionInfo = Schema.Struct({ + 544 16 : id: SessionID, + 545 22 : slug: Schema.String, + 546 24 : projectID: Project.ID, + 547 37 : workspaceID: optional(WorkspaceID), + 548 27 : directory: Schema.String, + 549 32 : path: optional(Schema.String), + 550 32 : parentID: optional(SessionID), + 551 36 : summary: optional(SessionSummary), + 552 32 : cost: optional(Schema.Finite), + 553 34 : tokens: optional(SessionTokens), + 554 32 : share: optional(SessionShare), + 555 23 : title: Schema.String, + 556 33 : agent: optional(Schema.String), + 557 32 : model: optional(SessionModel), + 558 25 : version: Schema.String, + 559 63 : metadata: optional(Schema.Record(Schema.String, Schema.Any)), + 560 25 : time: Schema.Struct({ + 561 28 : created: NonNegativeInt, + 562 28 : updated: NonNegativeInt, + 563 41 : compacting: optional(NonNegativeInt), + 564 35 : archived: optional(Schema.Finite), + 565 5 : }), + 566 45 : permission: optional(PermissionV1.Ruleset), + 567 32 : revert: optional(SessionRevert), + 568 39 : }).annotate({ identifier: "Session" }) + 569 : export type SessionInfo = typeof SessionInfo.Type + 570 : + 571 17 : const events = { + 572 21 : Created: define({ + 573 31 : type: "session.created", + 574 12 : ...options, + 575 15 : schema: { + 576 27 : sessionID: SessionID, + 577 21 : info: SessionInfo, + 578 3 : }, + 579 5 : }), + 580 21 : Updated: define({ + 581 31 : type: "session.updated", + 582 12 : ...options, + 583 15 : schema: { + 584 27 : sessionID: SessionID, + 585 21 : info: SessionInfo, + 586 3 : }, + 587 5 : }), + 588 21 : Deleted: define({ + 589 31 : type: "session.deleted", + 590 12 : ...options, + 591 15 : schema: { + 592 27 : sessionID: SessionID, + 593 21 : info: SessionInfo, + 594 3 : }, + 595 5 : }), + 596 28 : MessageUpdated: define({ + 597 31 : type: "message.updated", + 598 12 : ...options, + 599 15 : schema: { + 600 27 : sessionID: SessionID, + 601 14 : info: Info, + 602 3 : }, + 603 5 : }), + 604 28 : MessageRemoved: define({ + 605 31 : type: "message.removed", + 606 12 : ...options, + 607 15 : schema: { + 608 27 : sessionID: SessionID, + 609 24 : messageID: MessageID, + 610 3 : }, + 611 5 : }), + 612 25 : PartUpdated: define({ + 613 36 : type: "message.part.updated", + 614 12 : ...options, + 615 15 : schema: { + 616 27 : sessionID: SessionID, + 617 17 : part: Part, + 618 23 : time: Schema.Finite, + 619 3 : }, + 620 5 : }), + 621 25 : PartRemoved: define({ + 622 36 : type: "message.part.removed", + 623 12 : ...options, + 624 15 : schema: { + 625 27 : sessionID: SessionID, + 626 27 : messageID: MessageID, + 627 18 : partID: PartID, + 628 3 : }, + 629 3 : }), + 630 2 : } + 631 : + 632 34 : export const PartDelta = define({ + 633 29 : type: "message.part.delta", + 634 13 : schema: { + 635 25 : sessionID: SessionID, + 636 25 : messageID: MessageID, + 637 19 : partID: PartID, + 638 25 : field: Schema.String, + 639 22 : delta: Schema.String, + 640 2 : }, + 641 3 : }) + 642 : + 643 29 : export const Diff = define({ + 644 23 : type: "session.diff", + 645 13 : schema: { + 646 25 : sessionID: SessionID, + 647 35 : diff: Schema.Array(FileDiff.Info), + 648 2 : }, + 649 3 : }) + 650 : + 651 30 : export const Error = define({ + 652 24 : type: "session.error", + 653 13 : schema: { + 654 42 : sessionID: Schema.optional(SessionID), + 655 31 : error: Assistant.fields.error, + 656 2 : }, + 657 3 : }) + 658 : + 659 26 : export const Event = { + 660 9 : ...events, + 661 12 : PartDelta, + 662 7 : Diff, + 663 8 : Error, + 664 23 : Definitions: inventory( + 665 16 : events.Created, + 666 16 : events.Updated, + 667 16 : events.Deleted, + 668 23 : events.MessageUpdated, + 669 23 : events.MessageRemoved, + 670 20 : events.PartUpdated, + 671 20 : events.PartRemoved, + 672 11 : PartDelta, + 673 6 : Diff, + 674 5 : Error, + 675 2 : ), + 676 1 : } ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 52 : export * as WorkspaceEvent from "./workspace-event" + 2 : + 3 32 : import { Schema } from "effect" + 4 32 : import { Event } from "./event" + 5 45 : import { WorkspaceID } from "./workspace-id" + 6 : + 7 48 : export const ConnectionStatus = Schema.Struct({ + 8 27 : workspaceID: WorkspaceID, + 9 78 : status: Schema.Literals(["connected", "connecting", "disconnected", "error"]), + 10 63 : }).annotate({ identifier: "WorkspaceEvent.ConnectionStatus" }) + 11 : export interface ConnectionStatus extends Schema.Schema.Type<typeof ConnectionStatus> {} + 12 : + 13 36 : export const Ready = Event.define({ + 14 26 : type: "workspace.ready", + 15 13 : schema: { + 16 21 : name: Schema.String, + 17 2 : }, + 18 3 : }) + 19 : + 20 37 : export const Failed = Event.define({ + 21 27 : type: "workspace.failed", + 22 13 : schema: { + 23 24 : message: Schema.String, + 24 2 : }, + 25 3 : }) + 26 : + 27 37 : export const Status = Event.define({ + 28 27 : type: "workspace.status", + 29 32 : schema: ConnectionStatus.fields, + 30 3 : }) + 31 : + 32 65 : export const Definitions = Event.inventory(Ready, Failed, Status) ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 32 : import { Schema } from "effect" + 2 41 : import { ascending } from "./identifier" + 3 35 : import { statics } from "./schema" + 4 : + 5 79 : export const WorkspaceID = Schema.String.check(Schema.isStartsWith("wrk")).pipe( + 6 32 : Schema.brand("WorkspaceV2.ID"), + 7 22 : statics((schema) => { + 8 55 : const create = () => schema.make("wrk_" + ascending()) + 9 12 : return { + 10 25 : ascending: (id?: string) => { + 11 30 : if (!id) return create() + 12 0 : if (!id.startsWith("wrk")) throw new Error(`ID ${id} does not start with wrk`) + 13 5 : return schema.make(id) + 14 : }, + 15 8 : create, + 16 1 : } + 17 1 : }), + 18 2 : ) + 19 : export type WorkspaceID = typeof WorkspaceID.Type ++ |
+
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+Line data Source code+ + 1 41 : export * as Workspace from "./workspace" + 2 : + 3 51 : import { WorkspaceEvent } from "./workspace-event" + 4 45 : import { WorkspaceID } from "./workspace-id" + 5 : + 6 30 : export const ID = WorkspaceID + 7 : export type ID = WorkspaceID + 8 : + 9 35 : export const Event = WorkspaceEvent ++ |
+
| Generated by: LCOV version 2.0-1 |