feat(http-recorder): prepare public beta release (#31018)

This commit is contained in:
Kit Langton
2026-06-06 03:01:26 +00:00
committed by GitHub
parent ba57718b05
commit 54f4974546
36 changed files with 2277 additions and 713 deletions
@@ -1,4 +1,3 @@
import { Redactor } from "@opencode-ai/http-recorder"
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { CacheHint, LLM } from "../../src"
@@ -33,7 +32,7 @@ const recorded = recordedTests({
// Two identical requests in one cassette — replay walks the cassette in
// recording order so the second call replays the cached-hit interaction.
options: {
redactor: Redactor.defaults({ requestHeaders: { allow: ["content-type", "anthropic-version"] } }),
redact: { allowRequestHeaders: ["anthropic-version"] },
},
})
@@ -1,4 +1,3 @@
import { Redactor } from "@opencode-ai/http-recorder"
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMError, Message, ToolCallPart } from "../../src"
@@ -30,7 +29,7 @@ const recorded = recordedTests({
provider: "anthropic",
protocol: "anthropic-messages",
requires: ["ANTHROPIC_API_KEY"],
options: { redactor: Redactor.defaults({ requestHeaders: { allow: ["content-type", "anthropic-version"] } }) },
options: { redact: { allowRequestHeaders: ["anthropic-version"] } },
})
describe("Anthropic Messages sad-path recorded", () => {
@@ -1,4 +1,3 @@
import { Redactor } from "@opencode-ai/http-recorder"
import * as Anthropic from "../../src/providers/anthropic"
import { CloudflareAIGateway, CloudflareWorkersAI } from "../../src/providers/cloudflare"
import * as Google from "../../src/providers/google"
@@ -64,7 +63,7 @@ const redactCloudflareURL = (url: string) =>
.replace(/\/v1\/[^/]+\/[^/]+\/compat\//, "/v1/{account}/{gateway}/compat/")
const cloudflareOptions = {
redactor: Redactor.defaults({ url: { transform: redactCloudflareURL } }),
redact: { url: redactCloudflareURL },
}
describeRecordedGoldenScenarios([
@@ -103,7 +102,7 @@ describeRecordedGoldenScenarios([
prefix: "anthropic-messages",
model: anthropicHaiku,
requires: ["ANTHROPIC_API_KEY"],
options: { redactor: Redactor.defaults({ requestHeaders: { allow: ["content-type", "anthropic-version"] } }) },
options: { redact: { allowRequestHeaders: ["anthropic-version"] } },
scenarios: ["text", "tool-call"],
},
{
@@ -112,7 +111,7 @@ describeRecordedGoldenScenarios([
model: anthropicOpus,
requires: ["ANTHROPIC_API_KEY"],
tags: ["flagship"],
options: { redactor: Redactor.defaults({ requestHeaders: { allow: ["content-type", "anthropic-version"] } }) },
options: { redact: { allowRequestHeaders: ["anthropic-version"] } },
scenarios: [
{ id: "tool-loop", temperature: false },
{ id: "image-tool-result", temperature: false, maxTokens: 40 },
+1 -1
View File
@@ -29,7 +29,7 @@ type TargetInput = {
readonly prefix?: string
readonly tags?: ReadonlyArray<string>
readonly metadata?: Record<string, unknown>
readonly options?: HttpRecorder.RecordReplayOptions
readonly options?: HttpRecorder.RecorderOptions
readonly scenarios: ReadonlyArray<ScenarioInput>
}
+29 -9
View File
@@ -1,5 +1,6 @@
import { NodeFileSystem } from "@effect/platform-node"
import { HttpRecorder } from "@opencode-ai/http-recorder"
import { HttpRecorderInternal } from "@opencode-ai/http-recorder/internal"
import { Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import * as path from "node:path"
@@ -21,16 +22,16 @@ const FIXTURES_DIR = path.resolve(__dirname, "fixtures", "recordings")
type RecordedEnv = RequestExecutorService | WebSocketExecutorService | LLMClientService
type RecordedTestsOptions = RecordedGroupOptions & {
readonly options?: HttpRecorder.RecordReplayOptions
readonly options?: HttpRecorder.RecorderOptions
}
type RecordedCaseOptions = RunnerCaseOptions & {
readonly options?: HttpRecorder.RecordReplayOptions
readonly options?: HttpRecorder.RecorderOptions
}
const mergeOptions = (
base: HttpRecorder.RecordReplayOptions | undefined,
override: HttpRecorder.RecordReplayOptions | undefined,
base: HttpRecorder.RecorderOptions | undefined,
override: HttpRecorder.RecorderOptions | undefined,
) => {
if (!base) return override
if (!override) return base
@@ -38,6 +39,24 @@ const mergeOptions = (
...base,
...override,
metadata: base.metadata || override.metadata ? { ...base.metadata, ...override.metadata } : undefined,
redact:
base.redact || override.redact
? {
...base.redact,
...override.redact,
headers: [...(base.redact?.headers ?? []), ...(override.redact?.headers ?? [])],
allowRequestHeaders: [
...(base.redact?.allowRequestHeaders ?? []),
...(override.redact?.allowRequestHeaders ?? []),
],
allowResponseHeaders: [
...(base.redact?.allowResponseHeaders ?? []),
...(override.redact?.allowResponseHeaders ?? []),
],
queryParameters: [...(base.redact?.queryParameters ?? []), ...(override.redact?.queryParameters ?? [])],
jsonFields: [...(base.redact?.jsonFields ?? []), ...(override.redact?.jsonFields ?? [])],
}
: undefined,
}
}
@@ -45,23 +64,24 @@ export const recordedTests = (options: RecordedTestsOptions) =>
recordedEffectGroup<RecordedEnv, never, RecordedTestsOptions, RecordedCaseOptions>({
duplicateLabel: "recorded cassette",
options,
cassetteExists: (cassette) => HttpRecorder.hasCassetteSync(cassette, { directory: FIXTURES_DIR }),
cassetteExists: (cassette) => HttpRecorderInternal.hasCassetteSync(cassette, { directory: FIXTURES_DIR }),
layer: ({ cassette, metadata, options, caseOptions, recording }) => {
const recorderOptions = mergeOptions(options.options, caseOptions.options)
const recorderMetadata = {
...recorderOptions?.metadata,
...metadata,
}
const mode = recorderOptions?.mode ?? (recording ? "record" : "replay")
const cassetteService = HttpRecorder.Cassette.fileSystem({ directory: FIXTURES_DIR }).pipe(
const mode = recording ? "record" : "replay"
const cassetteService = HttpRecorderInternal.Cassette.fileSystem({ directory: FIXTURES_DIR }).pipe(
Layer.provide(NodeFileSystem.layer),
)
const requestExecutor = RequestExecutor.layer.pipe(
Layer.provide(
HttpRecorder.recordingLayer(cassette, {
...recorderOptions,
HttpRecorderInternal.recordingLayer(cassette, {
mode,
metadata: recorderMetadata,
redactor: HttpRecorderInternal.Redactor.make(recorderOptions?.redact),
match: recorderOptions?.match,
}).pipe(Layer.provide(FetchHttpClient.layer)),
),
)
+5 -5
View File
@@ -1,4 +1,4 @@
import { Cassette, makeWebSocketExecutor, type RecordReplayMode } from "@opencode-ai/http-recorder"
import { HttpRecorderInternal } from "@opencode-ai/http-recorder/internal"
import { Effect, Layer } from "effect"
import { WebSocketExecutor } from "../src/route"
import type { Service as WebSocketExecutorService } from "../src/route/transport/websocket"
@@ -7,13 +7,13 @@ const liveWebSocket = WebSocketExecutor.open
export const webSocketCassetteLayer = (
cassette: string,
input: { readonly metadata?: Record<string, unknown>; readonly mode: RecordReplayMode },
): Layer.Layer<WebSocketExecutorService, never, Cassette.Service> =>
input: { readonly metadata?: Record<string, unknown>; readonly mode: HttpRecorderInternal.RecordReplayMode },
): Layer.Layer<WebSocketExecutorService, never, HttpRecorderInternal.Cassette.Service> =>
Layer.effect(
WebSocketExecutor.Service,
Effect.gen(function* () {
const cassetteService = yield* Cassette.Service
const executor = yield* makeWebSocketExecutor({
const cassetteService = yield* HttpRecorderInternal.Cassette.Service
const executor = yield* HttpRecorderInternal.makeWebSocketExecutor({
name: cassette,
mode: input.mode,
metadata: input.metadata,