feat(http-recorder): prepare public beta release (#31018)
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user