chore: move aibridge library code into coder repo (#24190)

This PR merges code from `coder/aibridge` repository into `coder/coder`.
It was split into 4 PRs for easier review but stacked PRs will need to
be merged into this PR so all checks pass.

* https://github.com/coder/coder/pull/24190 -> raw code copy (this PR,
before merging PRs on top of it, it was just 1 commit:
https://github.com/coder/coder/commit/70d33f33200c7e77df910957595715f81f9bec24)
* https://github.com/coder/coder/pull/24570 -> update imports in
`coder/coder` to use copied code
* https://github.com/coder/coder/pull/24586 -> linter fixes and CI
integration (also added README.md)
* https://github.com/coder/coder/pull/24571 -> added exclude to
scripts/check_emdash.sh check

Original PR message (before PR squash):
Moves coder/aibridge code into coder/coder repository.

Omitted files:

- `go.mod`, `go.sum`, `.gitignore`, `.github/workflows/ci.yml,`
`Makefile`, `LICENSE`, `README.md` (modified README.md is added later)
- `.github`, `example`, `buildinfo,` `scripts` directories

Simple verification script (will list omitted files)

```
tmp=$(mktemp -d)
echo "$tmp"
git clone --depth=1 https://github.com/coder/aibridge "$tmp/aibridge"
git clone --depth=1 --branch pb/aibridge-code-move https://github.com/coder/coder "$tmp/coder"
diff -rq --exclude=.git "$tmp/aibridge" "$tmp/coder/aibridge"
# rm -rf "$tmp"
```
This commit is contained in:
Paweł Banaszewski
2026-04-22 17:01:01 +02:00
committed by GitHub
parent ec91ac5427
commit e00e85765b
163 changed files with 32480 additions and 33 deletions
+1
View File
@@ -80,6 +80,7 @@ jobs:
# Main repo directories for completeness in case other files are
# touched:
- "agent/**"
- "aibridge/**"
- "cli/**"
- "cmd/**"
- "coderd/**"
+2
View File
@@ -57,4 +57,6 @@ extend-exclude = [
# notifications' golden files confuse the detector because of quoted-printable encoding
"coderd/notifications/testdata/**",
"agent/agentcontainers/testdata/devcontainercli/**",
# aibridge fixtures contain truncated streaming chunks that look like typos
"aibridge/fixtures/**",
]
+1 -1
View File
@@ -1,7 +1,7 @@
rules:
cache-poisoning:
ignore:
- "ci.yaml:187"
- "ci.yaml:188"
dangerous-triggers:
ignore:
# Both workflows use pull_request_target intentionally: they need
+16
View File
@@ -6,6 +6,21 @@ linters-settings:
# goal: 100
threshold: 412
depguard:
rules:
aibridge_import_isolation:
list-mode: lax
files:
- "aibridge/*.go"
- "aibridge/**/*.go"
allow:
- $gostd
- github.com/coder/coder/v2/aibridge
- github.com/coder/coder/v2/buildinfo
deny:
- pkg: github.com/coder/coder/v2
desc: aibridge code must not import coder packages outside aibridge; buildinfo is the only exception
exhaustruct:
include:
# Gradually extend to cover more of the codebase.
@@ -227,6 +242,7 @@ linters:
- asciicheck
- bidichk
- bodyclose
- depguard
- dogsled
- errcheck
- errname
+118
View File
@@ -0,0 +1,118 @@
# AI Agent Guidelines for aibridge
> For local overrides, create `AGENTS.local.md` (gitignored).
You are an experienced, pragmatic software engineer. Simple solutions
over clever ones. Readability is a primary concern.
## Tone & Relationship
We're colleagues. Push back on bad ideas and speak up when something
doesn't make sense. Honesty over agreeableness.
- Disagree when I'm wrong. Act as a critical peer reviewer.
- Call out bad ideas, unreasonable expectations, and mistakes.
- **Ask for clarification** instead of assuming. Say when you don't know something.
- Architectural decisions require discussion; routine fixes do not.
## Foundational Rules
- Doing it right is better than doing it fast.
- YAGNI. Don't add features we don't need right now.
- Make the smallest reasonable changes to achieve the goal.
- Reduce code duplication, even if it takes extra effort.
- Match the style of surrounding code. Consistency within a file matters.
- Fix bugs immediately when you find them.
## Essential Commands
| Task | Command | Notes |
|-------------|------------------|--------------------------------------------|
| Test | `make test` | All tests, no race detector |
| Test (race) | `make test-race` | CGO_ENABLED=1, use for CI |
| Coverage | `make coverage` | Prints summary to stdout |
| Format | `make fmt` | gofumpt; single file: `make fmt FILE=path` |
| Mocks | `make mocks` | Regenerate from `mcp/api.go` |
**Always use these commands** instead of running `go test` or `gofumpt` directly.
## Code Navigation
Use LSP tools (go to definition, find references, hover) **before** resorting to grep.
This codebase has 90+ Go files across multiple packages, so LSP is faster and more accurate.
## Architecture Overview
AI Bridge is a smart gateway that sits between AI clients (Claude Code, Cursor,
etc.) and upstream providers (Anthropic, OpenAI). It intercepts all AI traffic
to provide centralized authn/z, auditing, token attribution, and MCP tool
administration. It runs as part of `coderd` (the Coder control plane). Users
authenticate with their Coder session tokens.
```
┌─────────────┐ ┌──────────────────────────────────────────┐
│ AI Client │ │ aibridge │
│ (Claude Code,│────▶│ RequestBridge (http.Handler) │
│ Cursor) │ │ ├── Provider (Anthropic/OpenAI) │
└─────────────┘ │ ├── Interceptor (streaming/blocking) │
│ ├── Recorder (tokens, prompts, tools) │
│ └── MCP Proxy (tool injection) │
└──────────────┬───────────────────────────┘
┌──────────────┐
│ Upstream API │
│ (Anthropic, │
│ OpenAI) │
└──────────────┘
```
Key packages:
- `intercept/`: request/response interception, per-provider subdirs (`messages/`, `responses/`, `chatcompletions/`)
- `provider/`: upstream provider definitions (Anthropic, OpenAI, Copilot)
- `mcp/`: MCP protocol integration
- `circuitbreaker/`: circuit breaker for upstream calls
- `context/`: request-scoped context helpers
- `internal/integrationtest/`: integration tests with mock upstreams
## Go Patterns
- Follow the [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md).
- Use `gofumpt` for formatting (enforced by `make fmt`).
- Prefer table-driven tests.
- **Never use `time.Sleep` in tests**. Use `github.com/coder/quartz` or channels/contexts for synchronization.
- Use unique identifiers in tests: `fmt.Sprintf("test-%s-%d", t.Name(), time.Now().UnixNano())`.
- Test observable behavior, not implementation details.
## Streaming Code
This codebase heavily uses SSE streaming. When modifying interceptors:
- Always handle both blocking and streaming paths.
- Test with `*_test.go` files in the same package. They cover edge cases for chunked responses.
- Be careful with goroutine lifecycle. Ensure proper cleanup on context cancellation.
## Commit Style
```
type(scope): message
```
- `scope` = real package path (e.g., `intercept/messages`, `provider`, `circuitbreaker`)
- Comments: full sentences, max 80 chars, explain **why** not what.
## Do NOT
- Rewrite comments or refactor code that isn't related to your task.
- Remove context from error messages.
- Use `--no-verify` on git operations.
- Add `//nolint` without a justification comment.
- Introduce new dependencies without discussion.
## Common Pitfalls
| Problem | Fix |
|-------------------------------|------------------------------------------------------------------|
| Race in streaming tests | Use `t.Cleanup()` and proper synchronization, never `time.Sleep` |
| Mock not updated | Run `make mocks` after changing `mcp/api.go` |
| Formatting failures | Run `make fmt` before committing |
| `retract` directive in go.mod | Don't remove. It's intentional (v1.0.8 conflict marker) |
+93
View File
@@ -0,0 +1,93 @@
# aibridge
aibridge is an HTTP gateway that sits between AI clients and upstream AI providers (Anthropic, OpenAI). It intercepts requests to record token usage, prompts, and tool invocations per user. Optionally supports centralized [MCP](https://modelcontextprotocol.io/) tool injection with allowlist/denylist filtering.
## Architecture
```
┌─────────────────┐ ┌───────────────────────────────────────────┐
│ AI Client │ │ aibridge │
│ (Claude Code, │────▶│ ┌─────────────────┐ ┌─────────────┐ │
│ Cursor, etc.) │ │ │ RequestBridge │───▶│ Providers │ │
└─────────────────┘ │ │ (http.Handler) │ │ (Anthropic │ │
│ └─────────────────┘ │ OpenAI) │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │ ┌─────────────┐
│ ┌─────────────────┐ ┌─────────────┐ │ │ Upstream │
│ │ Recorder │◀───│ Interceptor │─── ───▶│ API │
│ │ (tokens, tools, │ │ (streaming/ │ │ │ (Anthropic │
│ │ prompts) │ │ blocking) │ │ │ OpenAI) │
│ └────────┬────────┘ └──────┬──────┘ │ └─────────────┘
│ │ │ │
│ ▼ ┌──────▼──────┐ │
│ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ │ MCP Proxy │ │
│ │ Database │ │ (tools) │ │
│ └ ─ ─ ─ ─ ─ ─ ─ ┘ └─────────────┘ │
└───────────────────────────────────────────┘
```
### Components
- **RequestBridge**: The main `http.Handler` that routes requests to providers
- **Provider**: Defines bridged routes (intercepted) and passthrough routes (proxied)
- **Interceptor**: Handles request/response processing and streaming
- **Recorder**: Interface for capturing usage data (tokens, prompts, tools)
- **MCP Proxy** (optional): Connects to MCP servers to list tool, inject them into requests, and invoke them in an inner agentic loop
## Request Flow
1. Client sends request to `/anthropic/v1/messages` or `/openai/v1/chat/completions`
2. **Actor extraction**: Request must have an actor in context (via `AsActor()`).
3. **Upstream call**: Request forwarded to the AI provider
4. **Response relay**: Response streamed/sent to client
5. **Recording**: Token usage, prompts, and tool invocations recorded
**With MCP enabled**: Tools from configured MCP servers are centrally defined and injected into requests (prefixed `bmcp_`). Allowlist/denylist regex patterns control which tools are available. When the model selects an injected tool, the gateway invokes it in an inner agentic loop, and continues the conversation loop until complete.
Passthrough routes (`/v1/models`, `/v1/messages/count_tokens`) are reverse-proxied directly.
## Observability
### Prometheus Metrics
Create metrics with `NewMetrics(prometheus.Registerer)`:
| Metric | Type | Description |
|-----------------------------------|-----------|-------------------------------|
| `interceptions_total` | Counter | Intercepted request count |
| `interceptions_inflight` | Gauge | Currently processing requests |
| `interceptions_duration_seconds` | Histogram | Request duration |
| `tokens_total` | Counter | Token usage (input/output) |
| `prompts_total` | Counter | User prompt count |
| `injected_tool_invocations_total` | Counter | MCP tool invocations |
| `passthrough_total` | Counter | Non-intercepted requests |
### Recorder Interface
Implement `Recorder` to persist usage data to your database:
- `aibridge_interceptions` - request metadata (provider, model, initiator, timestamps)
- `aibridge_token_usages` - input/output token counts per response
- `aibridge_user_prompts` - user prompts
- `aibridge_tool_usages` - tool invocations (injected and client-defined)
```go
type Recorder interface {
RecordInterception(ctx context.Context, req *InterceptionRecord) error
RecordInterceptionEnded(ctx context.Context, req *InterceptionRecordEnded) error
RecordTokenUsage(ctx context.Context, req *TokenUsageRecord) error
RecordPromptUsage(ctx context.Context, req *PromptUsageRecord) error
RecordToolUsage(ctx context.Context, req *ToolUsageRecord) error
}
```
## Supported Routes
| Provider | Route | Type |
|-----------|---------------------------------------|-----------------------|
| Anthropic | `/anthropic/v1/messages` | Bridged (intercepted) |
| Anthropic | `/anthropic/v1/models` | Passthrough |
| Anthropic | `/anthropic/v1/messages/count_tokens` | Passthrough |
| OpenAI | `/openai/v1/chat/completions` | Bridged (intercepted) |
| OpenAI | `/openai/v1/models` | Passthrough |
+66
View File
@@ -0,0 +1,66 @@
package aibridge
import (
"context"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/trace"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/config"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/metrics"
"github.com/coder/coder/v2/aibridge/provider"
"github.com/coder/coder/v2/aibridge/recorder"
)
// Const + Type + function aliases for backwards compatibility.
const (
ProviderAnthropic = config.ProviderAnthropic
ProviderOpenAI = config.ProviderOpenAI
ProviderCopilot = config.ProviderCopilot
)
type (
Metrics = metrics.Metrics
Provider = provider.Provider
InterceptionRecord = recorder.InterceptionRecord
InterceptionRecordEnded = recorder.InterceptionRecordEnded
TokenUsageRecord = recorder.TokenUsageRecord
PromptUsageRecord = recorder.PromptUsageRecord
ToolUsageRecord = recorder.ToolUsageRecord
ModelThoughtRecord = recorder.ModelThoughtRecord
Recorder = recorder.Recorder
Metadata = recorder.Metadata
AnthropicConfig = config.Anthropic
AWSBedrockConfig = config.AWSBedrock
OpenAIConfig = config.OpenAI
CopilotConfig = config.Copilot
)
func AsActor(ctx context.Context, actorID string, metadata recorder.Metadata) context.Context {
return aibcontext.AsActor(ctx, actorID, metadata)
}
func NewAnthropicProvider(cfg config.Anthropic, bedrockCfg *config.AWSBedrock) provider.Provider {
return provider.NewAnthropic(cfg, bedrockCfg)
}
func NewOpenAIProvider(cfg config.OpenAI) provider.Provider {
return provider.NewOpenAI(cfg)
}
func NewCopilotProvider(cfg config.Copilot) provider.Provider {
return provider.NewCopilot(cfg)
}
func NewMetrics(reg prometheus.Registerer) *metrics.Metrics {
return metrics.NewMetrics(reg)
}
func NewRecorder(logger slog.Logger, tracer trace.Tracer, clientFn func() (Recorder, error)) Recorder {
return recorder.NewWrappedRecorder(logger, tracer, clientFn)
}
+365
View File
@@ -0,0 +1,365 @@
package aibridge
import (
"context"
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/hashicorp/go-multierror"
"github.com/sony/gobreaker/v2"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/circuitbreaker"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/mcp"
"github.com/coder/coder/v2/aibridge/metrics"
"github.com/coder/coder/v2/aibridge/provider"
"github.com/coder/coder/v2/aibridge/recorder"
"github.com/coder/coder/v2/aibridge/tracing"
)
const (
// The duration after which an async recording will be aborted.
recordingTimeout = time.Second * 5
)
// RequestBridge is an [http.Handler] which is capable of masquerading as AI providers' APIs;
// specifically, OpenAI's & Anthropic's at present.
// RequestBridge intercepts requests to - and responses from - these upstream services to provide
// a centralized governance layer.
//
// RequestBridge has no concept of authentication or authorization. It does have a concept of identity,
// in the narrow sense that it expects an [actor] to be defined in the context, to record the initiator
// of each interception.
//
// RequestBridge is safe for concurrent use.
type RequestBridge struct {
mux *http.ServeMux
logger slog.Logger
mcpProxy mcp.ServerProxier
inflightReqs atomic.Int32
inflightWG sync.WaitGroup // For graceful shutdown.
inflightCtx context.Context
inflightCancel func()
shutdownOnce sync.Once
closed chan struct{}
}
var _ http.Handler = &RequestBridge{}
// validProviderName matches names containing only lowercase alphanumeric characters and hyphens.
var validProviderName = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
// validateProviders checks that provider names are valid and unique.
func validateProviders(providers []provider.Provider) error {
names := make(map[string]bool, len(providers))
for _, prov := range providers {
name := prov.Name()
if !validProviderName.MatchString(name) {
return xerrors.Errorf("invalid provider name %q: must contain only lowercase alphanumeric characters and hyphens", name)
}
if names[name] {
return xerrors.Errorf("duplicate provider name: %q", name)
}
names[name] = true
}
return nil
}
// NewRequestBridge creates a new *[RequestBridge] and registers the HTTP routes defined by the given providers.
// Any routes which are requested but not registered will be reverse-proxied to the upstream service.
//
// A [intercept.Recorder] is also required to record prompt, tool, and token use.
//
// mcpProxy will be closed when the [RequestBridge] is closed.
//
// Circuit breaker configuration is obtained from each provider's CircuitBreakerConfig() method.
// Providers returning nil will not have circuit breaker protection.
func NewRequestBridge(ctx context.Context, providers []provider.Provider, rec recorder.Recorder, mcpProxy mcp.ServerProxier, logger slog.Logger, m *metrics.Metrics, tracer trace.Tracer) (*RequestBridge, error) {
if err := validateProviders(providers); err != nil {
return nil, err
}
mux := http.NewServeMux()
for _, prov := range providers {
// Create per-provider circuit breaker if configured
cfg := prov.CircuitBreakerConfig()
providerName := prov.Name()
onChange := func(endpoint, model string, from, to gobreaker.State) {
logger.Info(context.Background(), "circuit breaker state change",
slog.F("provider", providerName),
slog.F("endpoint", endpoint),
slog.F("model", model),
slog.F("from", from.String()),
slog.F("to", to.String()),
)
if m != nil {
m.CircuitBreakerState.WithLabelValues(providerName, endpoint, model).Set(circuitbreaker.StateToGaugeValue(to))
if to == gobreaker.StateOpen {
m.CircuitBreakerTrips.WithLabelValues(providerName, endpoint, model).Inc()
}
}
}
cbs := circuitbreaker.NewProviderCircuitBreakers(providerName, cfg, onChange, m)
// Add the known provider-specific routes which are bridged (i.e. intercepted and augmented).
for _, path := range prov.BridgedRoutes() {
handler := newInterceptionProcessor(prov, cbs, rec, mcpProxy, logger, m, tracer)
route, err := url.JoinPath(prov.RoutePrefix(), path)
if err != nil {
logger.Error(ctx, "failed to join path",
slog.Error(err),
slog.F("provider", providerName),
slog.F("prefix", prov.RoutePrefix()),
slog.F("path", path),
)
return nil, xerrors.Errorf("failed to configure provider '%v': failed to join bridged path: %w", providerName, err)
}
mux.Handle(route, handler)
}
// Any requests which passthrough to this will be reverse-proxied to the upstream.
//
// We have to whitelist the known-safe routes because an API key with elevated privileges (i.e. admin) might be
// configured, so we should just reverse-proxy known-safe routes.
ftr := newPassthroughRouter(prov, logger.Named(fmt.Sprintf("passthrough.%s", prov.Name())), m, tracer)
for _, path := range prov.PassthroughRoutes() {
route, err := url.JoinPath(prov.RoutePrefix(), path)
if err != nil {
logger.Error(ctx, "failed to join path",
slog.Error(err),
slog.F("provider", providerName),
slog.F("prefix", prov.RoutePrefix()),
slog.F("path", path),
)
return nil, xerrors.Errorf("failed to configure provider '%v': failed to join passed through path: %w", providerName, err)
}
mux.Handle(route, http.StripPrefix(prov.RoutePrefix(), ftr))
}
}
// Catch-all.
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
logger.Warn(r.Context(), "route not supported", slog.F("path", r.URL.Path), slog.F("method", r.Method))
http.Error(w, fmt.Sprintf("route not supported: %s %s", r.Method, r.URL.Path), http.StatusNotFound)
})
inflightCtx, cancel := context.WithCancel(context.Background())
return &RequestBridge{
mux: mux,
logger: logger,
mcpProxy: mcpProxy,
inflightCtx: inflightCtx,
inflightCancel: cancel,
closed: make(chan struct{}, 1),
}, nil
}
// newInterceptionProcessor returns an [http.HandlerFunc] which is capable of creating a new interceptor and processing a given request
// using [Provider] p, recording all usage events using [Recorder] rec.
// If cbs is non-nil, circuit breaker protection is applied per endpoint/model tuple.
func newInterceptionProcessor(p provider.Provider, cbs *circuitbreaker.ProviderCircuitBreakers, rec recorder.Recorder, mcpProxy mcp.ServerProxier, logger slog.Logger, m *metrics.Metrics, tracer trace.Tracer) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "Intercept")
defer span.End()
// We execute this before CreateInterceptor since the interceptors
// read the request body and don't reset them.
client := GuessClient(r)
sessionID := GuessSessionID(client, r)
interceptor, err := p.CreateInterceptor(w, r.WithContext(ctx), tracer)
if err != nil {
span.SetStatus(codes.Error, fmt.Sprintf("failed to create interceptor: %v", err))
logger.Warn(ctx, "failed to create interceptor", slog.Error(err), slog.F("path", r.URL.Path))
http.Error(w, fmt.Sprintf("failed to create %q interceptor", r.URL.Path), http.StatusInternalServerError)
return
}
if m != nil {
start := time.Now()
defer func() {
m.InterceptionDuration.WithLabelValues(p.Name(), interceptor.Model()).Observe(time.Since(start).Seconds())
}()
}
actor := aibcontext.ActorFromContext(ctx)
if actor == nil {
logger.Warn(ctx, "no actor found in context")
http.Error(w, "no actor found", http.StatusBadRequest)
return
}
traceAttrs := interceptor.TraceAttributes(r)
span.SetAttributes(traceAttrs...)
ctx = tracing.WithInterceptionAttributesInContext(ctx, traceAttrs)
r = r.WithContext(ctx)
// Record usage in the background to not block request flow.
asyncRecorder := recorder.NewAsyncRecorder(logger, rec, recordingTimeout)
asyncRecorder.WithMetrics(m)
asyncRecorder.WithProvider(p.Name())
asyncRecorder.WithModel(interceptor.Model())
asyncRecorder.WithInitiatorID(actor.ID)
asyncRecorder.WithClient(string(client))
interceptor.Setup(logger, asyncRecorder, mcpProxy)
cred := interceptor.Credential()
if err := rec.RecordInterception(ctx, &recorder.InterceptionRecord{
ID: interceptor.ID().String(),
InitiatorID: actor.ID,
Metadata: actor.Metadata,
Model: interceptor.Model(),
Provider: p.Type(),
ProviderName: p.Name(),
UserAgent: r.UserAgent(),
Client: string(client),
ClientSessionID: sessionID,
CorrelatingToolCallID: interceptor.CorrelatingToolCallID(),
CredentialKind: string(cred.Kind),
CredentialHint: cred.Hint,
}); err != nil {
span.SetStatus(codes.Error, fmt.Sprintf("failed to record interception: %v", err))
logger.Warn(ctx, "failed to record interception", slog.Error(err))
http.Error(w, "failed to record interception", http.StatusInternalServerError)
return
}
route := strings.TrimPrefix(r.URL.Path, fmt.Sprintf("/%s", p.Name()))
log := logger.With(
slog.F("route", route),
slog.F("provider", p.Name()),
slog.F("interception_id", interceptor.ID()),
slog.F("user_agent", r.UserAgent()),
slog.F("streaming", interceptor.Streaming()),
slog.F("credential_kind", string(cred.Kind)),
slog.F("credential_hint", cred.Hint),
slog.F("credential_length", cred.Length),
)
log.Debug(ctx, "interception started")
if m != nil {
m.InterceptionsInflight.WithLabelValues(p.Name(), interceptor.Model(), route).Add(1)
defer func() {
m.InterceptionsInflight.WithLabelValues(p.Name(), interceptor.Model(), route).Sub(1)
}()
}
// Process request with circuit breaker protection if configured
if err := cbs.Execute(route, interceptor.Model(), w, func(rw http.ResponseWriter) error {
return interceptor.ProcessRequest(rw, r)
}); err != nil {
if m != nil {
m.InterceptionCount.WithLabelValues(p.Name(), interceptor.Model(), metrics.InterceptionCountStatusFailed, route, r.Method, actor.ID, string(client)).Add(1)
}
span.SetStatus(codes.Error, fmt.Sprintf("interception failed: %v", err))
log.Warn(ctx, "interception failed", slog.Error(err))
} else {
if m != nil {
m.InterceptionCount.WithLabelValues(p.Name(), interceptor.Model(), metrics.InterceptionCountStatusCompleted, route, r.Method, actor.ID, string(client)).Add(1)
}
log.Debug(ctx, "interception ended")
}
_ = asyncRecorder.RecordInterceptionEnded(ctx, &recorder.InterceptionRecordEnded{ID: interceptor.ID().String()})
// Ensure all recording have completed before completing request.
asyncRecorder.Wait()
}
}
// ServeHTTP exposes the internal http.Handler, which has all [Provider]s' routes registered.
// It also tracks inflight requests.
func (b *RequestBridge) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
select {
case <-b.closed:
http.Error(rw, "server closed", http.StatusInternalServerError)
return
default:
}
// We want to abide by the context passed in without losing any of its
// functionality, but we still want to link our shutdown context to each
// request.
ctx := mergeContexts(r.Context(), b.inflightCtx)
b.inflightReqs.Add(1)
b.inflightWG.Add(1)
defer func() {
b.inflightReqs.Add(-1)
b.inflightWG.Done()
}()
b.mux.ServeHTTP(rw, r.WithContext(ctx))
}
// Shutdown will attempt to gracefully shutdown. This entails waiting for all requests to
// complete, and shutting down the MCP server proxier.
// TODO: add tests.
func (b *RequestBridge) Shutdown(ctx context.Context) error {
var err error
b.shutdownOnce.Do(func() {
// Prevent any new requests from being accepted.
close(b.closed)
// Wait for inflight requests to complete or context cancellation.
done := make(chan struct{})
go func() {
b.inflightWG.Wait()
close(done)
}()
select {
case <-ctx.Done():
// Cancel all inflight requests, if any are still running.
b.logger.Debug(ctx, "shutdown context canceled; canceling inflight requests", slog.Error(ctx.Err()))
b.inflightCancel()
<-done
err = ctx.Err()
case <-done:
}
if b.mcpProxy != nil {
// It's ok that we reuse the ctx here even if it's done, since the
// Shutdown method will just immediately use the more aggressive close
// since the ctx is already expired.
err = multierror.Append(err, b.mcpProxy.Shutdown(ctx))
}
})
return err
}
func (b *RequestBridge) InflightRequests() int32 {
return b.inflightReqs.Load()
}
// mergeContexts merges two contexts together, so that if either is canceled
// the returned context is canceled. The context values will only be used from
// the first context.
func mergeContexts(base, other context.Context) context.Context {
ctx, cancel := context.WithCancel(base)
go func() {
defer cancel()
select {
case <-base.Done():
case <-other.Done():
}
}()
return ctx
}
+207
View File
@@ -0,0 +1,207 @@
package aibridge_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/aibridge"
"github.com/coder/coder/v2/aibridge/config"
"github.com/coder/coder/v2/aibridge/internal/testutil"
"github.com/coder/coder/v2/aibridge/provider"
)
var bridgeTestTracer = otel.Tracer("bridge_test")
func TestValidateProviders(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
tests := []struct {
name string
providers []provider.Provider
expectErr string
}{
{
name: "all_supported_providers",
providers: []provider.Provider{
aibridge.NewOpenAIProvider(config.OpenAI{Name: "openai", BaseURL: "https://api.openai.com/v1/"}),
aibridge.NewAnthropicProvider(config.Anthropic{Name: "anthropic", BaseURL: "https://api.anthropic.com/"}, nil),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot", BaseURL: "https://api.individual.githubcopilot.com"}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot-business", BaseURL: "https://api.business.githubcopilot.com"}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot-enterprise", BaseURL: "https://api.enterprise.githubcopilot.com"}),
},
},
{
name: "default_names_and_base_urls",
providers: []provider.Provider{
aibridge.NewOpenAIProvider(config.OpenAI{}),
aibridge.NewAnthropicProvider(config.Anthropic{}, nil),
aibridge.NewCopilotProvider(config.Copilot{}),
},
},
{
name: "multiple_copilot_instances",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot-business", BaseURL: "https://api.business.githubcopilot.com"}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot-enterprise", BaseURL: "https://api.enterprise.githubcopilot.com"}),
},
},
{
name: "name_with_slashes",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot/business", BaseURL: "https://api.business.githubcopilot.com"}),
},
expectErr: "invalid provider name",
},
{
name: "name_with_spaces",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot business", BaseURL: "https://api.business.githubcopilot.com"}),
},
expectErr: "invalid provider name",
},
{
name: "name_with_uppercase",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{Name: "Copilot", BaseURL: "https://api.business.githubcopilot.com"}),
},
expectErr: "invalid provider name",
},
{
name: "unique_names",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot", BaseURL: "https://api.individual.githubcopilot.com"}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot-business", BaseURL: "https://api.business.githubcopilot.com"}),
},
},
{
name: "duplicate_base_url_different_names",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot", BaseURL: "https://api.individual.githubcopilot.com"}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot-business", BaseURL: "https://api.individual.githubcopilot.com"}),
},
},
{
name: "duplicate_name",
providers: []provider.Provider{
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot", BaseURL: "https://api.individual.githubcopilot.com"}),
aibridge.NewCopilotProvider(config.Copilot{Name: "copilot", BaseURL: "https://api.business.githubcopilot.com"}),
},
expectErr: "duplicate provider name",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := aibridge.NewRequestBridge(t.Context(), tc.providers, nil, nil, logger, nil, bridgeTestTracer)
if tc.expectErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectErr)
} else {
require.NoError(t, err)
}
})
}
}
func TestPassthroughRoutesForProviders(t *testing.T) {
t.Parallel()
upstreamRespBody := "upstream response"
tests := []struct {
name string
baseURLPath string
requestPath string
provider func(string) provider.Provider
expectPath string
}{
{
name: "openAI_no_base_path",
requestPath: "/openai/v1/conversations",
provider: func(baseURL string) provider.Provider {
return aibridge.NewOpenAIProvider(config.OpenAI{BaseURL: baseURL})
},
expectPath: "/conversations",
},
{
name: "openAI_with_base_path",
baseURLPath: "/v1",
requestPath: "/openai/v1/conversations",
provider: func(baseURL string) provider.Provider {
return aibridge.NewOpenAIProvider(config.OpenAI{BaseURL: baseURL})
},
expectPath: "/v1/conversations",
},
{
name: "anthropic_no_base_path",
requestPath: "/anthropic/v1/models",
provider: func(baseURL string) provider.Provider {
return aibridge.NewAnthropicProvider(config.Anthropic{BaseURL: baseURL}, nil)
},
expectPath: "/v1/models",
},
{
name: "anthropic_with_base_path",
baseURLPath: "/v1",
requestPath: "/anthropic/v1/models",
provider: func(baseURL string) provider.Provider {
return aibridge.NewAnthropicProvider(config.Anthropic{BaseURL: baseURL}, nil)
},
expectPath: "/v1/v1/models",
},
{
name: "copilot_no_base_path",
requestPath: "/copilot/models",
provider: func(baseURL string) provider.Provider {
return aibridge.NewCopilotProvider(config.Copilot{BaseURL: baseURL})
},
expectPath: "/models",
},
{
name: "copilot_with_base_path",
baseURLPath: "/v1",
requestPath: "/copilot/models",
provider: func(baseURL string) provider.Provider {
return aibridge.NewCopilotProvider(config.Copilot{BaseURL: baseURL})
},
expectPath: "/v1/models",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, tc.expectPath, r.URL.Path)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(upstreamRespBody))
}))
t.Cleanup(upstream.Close)
rec := testutil.MockRecorder{}
prov := tc.provider(upstream.URL + tc.baseURLPath)
bridge, err := aibridge.NewRequestBridge(t.Context(), []provider.Provider{prov}, &rec, nil, logger, nil, bridgeTestTracer)
require.NoError(t, err)
req := httptest.NewRequest("", tc.requestPath, nil)
resp := httptest.NewRecorder()
bridge.ServeHTTP(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), upstreamRespBody)
})
}
}
+217
View File
@@ -0,0 +1,217 @@
package circuitbreaker
import (
"bufio"
"errors"
"fmt"
"net"
"net/http"
"sync"
"time"
"github.com/sony/gobreaker/v2"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/aibridge/config"
"github.com/coder/coder/v2/aibridge/metrics"
)
// ErrCircuitOpen is returned by Execute when the circuit breaker is open
// and the request was rejected without calling the handler.
var ErrCircuitOpen = xerrors.New("circuit breaker is open")
// DefaultIsFailure returns true for standard HTTP status codes that typically
// indicate upstream overload.
func DefaultIsFailure(statusCode int) bool {
switch statusCode {
case http.StatusTooManyRequests, // 429
http.StatusServiceUnavailable, // 503
http.StatusGatewayTimeout: // 504
return true
default:
return false
}
}
// ProviderCircuitBreakers manages per-endpoint/model circuit breakers for a single provider.
type ProviderCircuitBreakers struct {
provider string
config config.CircuitBreaker
breakers sync.Map // "endpoint:model" -> *gobreaker.CircuitBreaker[struct{}]
onChange func(endpoint, model string, from, to gobreaker.State)
metrics *metrics.Metrics
}
// NewProviderCircuitBreakers creates circuit breakers for a single provider.
// Returns nil if cfg is nil (no circuit breaker protection).
// onChange is called when circuit state changes.
// metrics is used to record circuit breaker reject counts (can be nil).
func NewProviderCircuitBreakers(provider string, cfg *config.CircuitBreaker, onChange func(endpoint, model string, from, to gobreaker.State), m *metrics.Metrics) *ProviderCircuitBreakers {
if cfg == nil {
return nil
}
return &ProviderCircuitBreakers{
provider: provider,
config: *cfg,
onChange: onChange,
metrics: m,
}
}
// isFailure checks if the status code should count as a failure.
// Falls back to DefaultIsFailure if no custom function is configured.
func (p *ProviderCircuitBreakers) isFailure(statusCode int) bool {
if p.config.IsFailure != nil {
return p.config.IsFailure(statusCode)
}
return DefaultIsFailure(statusCode)
}
// openErrBody returns the error response body when the circuit is open.
func (p *ProviderCircuitBreakers) openErrBody() []byte {
if p.config.OpenErrorResponse != nil {
return p.config.OpenErrorResponse()
}
return []byte(`{"error":"circuit breaker is open"}`)
}
// Get returns the circuit breaker for an endpoint/model tuple, creating it if needed.
func (p *ProviderCircuitBreakers) Get(endpoint, model string) *gobreaker.CircuitBreaker[struct{}] {
key := endpoint + ":" + model
if v, ok := p.breakers.Load(key); ok {
return v.(*gobreaker.CircuitBreaker[struct{}]) //nolint:forcetypeassert // sync.Map always stores this type
}
settings := gobreaker.Settings{
Name: p.provider + ":" + key,
MaxRequests: p.config.MaxRequests,
Interval: p.config.Interval,
Timeout: p.config.Timeout,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures >= p.config.FailureThreshold
},
OnStateChange: func(_ string, from, to gobreaker.State) {
if p.onChange != nil {
p.onChange(endpoint, model, from, to)
}
},
}
cb := gobreaker.NewCircuitBreaker[struct{}](settings)
actual, _ := p.breakers.LoadOrStore(key, cb)
return actual.(*gobreaker.CircuitBreaker[struct{}]) //nolint:forcetypeassert // sync.Map always stores this type
}
// statusCapturingWriter wraps http.ResponseWriter to capture the status code.
// It implements http.Flusher to support streaming and http.Hijacker to
// satisfy the FullResponseWriter lint rule.
type statusCapturingWriter struct {
http.ResponseWriter
statusCode int
headerWritten bool
}
func (w *statusCapturingWriter) WriteHeader(code int) {
if !w.headerWritten {
w.statusCode = code
w.headerWritten = true
}
w.ResponseWriter.WriteHeader(code)
}
func (w *statusCapturingWriter) Write(b []byte) (int, error) {
if !w.headerWritten {
w.statusCode = http.StatusOK
w.headerWritten = true
}
return w.ResponseWriter.Write(b)
}
func (w *statusCapturingWriter) Flush() {
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
func (w *statusCapturingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := w.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, xerrors.New("upstream ResponseWriter does not support hijacking")
}
return h.Hijack()
}
// Unwrap returns the underlying ResponseWriter for interface checks.
func (w *statusCapturingWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}
// Execute runs the given handler function within circuit breaker protection.
// If the circuit is open, the request is rejected with a 503 response, metrics are recorded,
// and ErrCircuitOpen is returned.
// Otherwise, it returns the handler's error (or nil on success).
// The handler receives a wrapped ResponseWriter that captures the status code.
// If the receiver is nil (no circuit breaker configured), the handler is called directly.
func (p *ProviderCircuitBreakers) Execute(endpoint, model string, w http.ResponseWriter, handler func(http.ResponseWriter) error) error {
if p == nil {
return handler(w)
}
cb := p.Get(endpoint, model)
// Wrap response writer to capture status code
sw := &statusCapturingWriter{ResponseWriter: w, statusCode: http.StatusOK}
var handlerErr error
_, err := cb.Execute(func() (struct{}, error) {
handlerErr = handler(sw)
if p.isFailure(sw.statusCode) {
return struct{}{}, xerrors.Errorf("upstream error: %d", sw.statusCode)
}
return struct{}{}, nil
})
if errors.Is(err, gobreaker.ErrOpenState) || errors.Is(err, gobreaker.ErrTooManyRequests) {
if p.metrics != nil {
p.metrics.CircuitBreakerRejects.WithLabelValues(p.provider, endpoint, model).Inc()
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Retry-After", fmt.Sprintf("%d", int64(p.config.Timeout.Seconds())))
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = w.Write(p.openErrBody())
return ErrCircuitOpen
}
return handlerErr
}
// Timeout returns the configured timeout duration for this circuit breaker.
func (p *ProviderCircuitBreakers) Timeout() time.Duration {
return p.config.Timeout
}
// Provider returns the provider name for this circuit breaker.
func (p *ProviderCircuitBreakers) Provider() string {
return p.provider
}
// OpenErrorResponse returns the error response body when the circuit is open.
// This is exposed for handlers to use when responding to rejected requests.
func (p *ProviderCircuitBreakers) OpenErrorResponse() []byte {
return p.openErrBody()
}
// StateToGaugeValue converts gobreaker.State to a gauge value.
// closed=0, half-open=0.5, open=1
func StateToGaugeValue(s gobreaker.State) float64 {
switch s {
case gobreaker.StateClosed:
return 0
case gobreaker.StateHalfOpen:
return 0.5
case gobreaker.StateOpen:
return 1
default:
return 0
}
}
@@ -0,0 +1,223 @@
package circuitbreaker_test
import (
"errors"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"github.com/sony/gobreaker/v2"
"github.com/stretchr/testify/assert"
"github.com/coder/coder/v2/aibridge/circuitbreaker"
"github.com/coder/coder/v2/aibridge/config"
)
func TestExecute_PerModelIsolation(t *testing.T) {
t.Parallel()
sonnetCalls := atomic.Int32{}
haikuCalls := atomic.Int32{}
cbs := circuitbreaker.NewProviderCircuitBreakers("test", &config.CircuitBreaker{
FailureThreshold: 1,
Interval: time.Minute,
Timeout: time.Minute,
MaxRequests: 1,
}, func(endpoint, model string, from, to gobreaker.State) {}, nil)
endpoint := "/v1/messages"
sonnetModel := "claude-sonnet-4-20250514"
haikuModel := "claude-3-5-haiku-20241022"
// Trip circuit on sonnet model (returns 429)
w := httptest.NewRecorder()
err := cbs.Execute(endpoint, sonnetModel, w, func(rw http.ResponseWriter) error {
sonnetCalls.Add(1)
rw.WriteHeader(http.StatusTooManyRequests)
return nil
})
assert.NoError(t, err)
assert.Equal(t, int32(1), sonnetCalls.Load())
// Second sonnet request should be blocked by circuit breaker
w = httptest.NewRecorder()
err = cbs.Execute(endpoint, sonnetModel, w, func(rw http.ResponseWriter) error {
sonnetCalls.Add(1)
rw.WriteHeader(http.StatusOK)
return nil
})
assert.True(t, errors.Is(err, circuitbreaker.ErrCircuitOpen))
assert.Equal(t, int32(1), sonnetCalls.Load()) // No new call
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
// Haiku model on same endpoint should still work (independent circuit)
w = httptest.NewRecorder()
err = cbs.Execute(endpoint, haikuModel, w, func(rw http.ResponseWriter) error {
haikuCalls.Add(1)
rw.WriteHeader(http.StatusOK)
return nil
})
assert.NoError(t, err)
assert.Equal(t, int32(1), haikuCalls.Load())
}
func TestExecute_PerEndpointIsolation(t *testing.T) {
t.Parallel()
messagesCalls := atomic.Int32{}
completionsCalls := atomic.Int32{}
cbs := circuitbreaker.NewProviderCircuitBreakers("test", &config.CircuitBreaker{
FailureThreshold: 1,
Interval: time.Minute,
Timeout: time.Minute,
MaxRequests: 1,
}, func(endpoint, model string, from, to gobreaker.State) {}, nil)
model := "test-model"
// Trip circuit on /v1/messages endpoint (returns 429)
w := httptest.NewRecorder()
err := cbs.Execute("/v1/messages", model, w, func(rw http.ResponseWriter) error {
messagesCalls.Add(1)
rw.WriteHeader(http.StatusTooManyRequests)
return nil
})
assert.NoError(t, err)
assert.Equal(t, int32(1), messagesCalls.Load())
// Second /v1/messages request should be blocked
w = httptest.NewRecorder()
err = cbs.Execute("/v1/messages", model, w, func(rw http.ResponseWriter) error {
messagesCalls.Add(1)
rw.WriteHeader(http.StatusOK)
return nil
})
assert.True(t, errors.Is(err, circuitbreaker.ErrCircuitOpen))
assert.Equal(t, int32(1), messagesCalls.Load()) // No new call
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
// /v1/chat/completions on same model should still work (different endpoint)
w = httptest.NewRecorder()
err = cbs.Execute("/v1/chat/completions", model, w, func(rw http.ResponseWriter) error {
completionsCalls.Add(1)
rw.WriteHeader(http.StatusOK)
return nil
})
assert.NoError(t, err)
assert.Equal(t, int32(1), completionsCalls.Load())
}
func TestExecute_CustomIsFailure(t *testing.T) {
t.Parallel()
var calls atomic.Int32
// Custom IsFailure that treats 502 as failure
cbs := circuitbreaker.NewProviderCircuitBreakers("test", &config.CircuitBreaker{
FailureThreshold: 1,
Interval: time.Minute,
Timeout: time.Minute,
MaxRequests: 1,
IsFailure: func(statusCode int) bool {
return statusCode == http.StatusBadGateway
},
}, func(endpoint, model string, from, to gobreaker.State) {}, nil)
// First request returns 502, trips circuit
w := httptest.NewRecorder()
err := cbs.Execute("/v1/messages", "test-model", w, func(rw http.ResponseWriter) error {
calls.Add(1)
rw.WriteHeader(http.StatusBadGateway)
return nil
})
assert.NoError(t, err)
assert.Equal(t, int32(1), calls.Load())
// Second request should be blocked
w = httptest.NewRecorder()
err = cbs.Execute("/v1/messages", "test-model", w, func(rw http.ResponseWriter) error {
calls.Add(1)
rw.WriteHeader(http.StatusOK)
return nil
})
assert.True(t, errors.Is(err, circuitbreaker.ErrCircuitOpen))
assert.Equal(t, int32(1), calls.Load()) // No new call
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
}
func TestExecute_OnStateChange(t *testing.T) {
t.Parallel()
var stateChanges []struct {
endpoint string
model string
from gobreaker.State
to gobreaker.State
}
cbs := circuitbreaker.NewProviderCircuitBreakers("test", &config.CircuitBreaker{
FailureThreshold: 1,
Interval: time.Minute,
Timeout: time.Minute,
MaxRequests: 1,
}, func(endpoint, model string, from, to gobreaker.State) {
stateChanges = append(stateChanges, struct {
endpoint string
model string
from gobreaker.State
to gobreaker.State
}{endpoint, model, from, to})
}, nil)
endpoint := "/v1/messages"
model := "claude-sonnet-4-20250514"
// Trip circuit
w := httptest.NewRecorder()
err := cbs.Execute(endpoint, model, w, func(rw http.ResponseWriter) error {
rw.WriteHeader(http.StatusTooManyRequests)
return nil
})
assert.NoError(t, err)
// Verify state change callback was called with correct parameters
assert.Len(t, stateChanges, 1)
assert.Equal(t, endpoint, stateChanges[0].endpoint)
assert.Equal(t, model, stateChanges[0].model)
assert.Equal(t, gobreaker.StateClosed, stateChanges[0].from)
assert.Equal(t, gobreaker.StateOpen, stateChanges[0].to)
}
func TestDefaultIsFailure(t *testing.T) {
t.Parallel()
tests := []struct {
statusCode int
isFailure bool
}{
{http.StatusOK, false},
{http.StatusBadRequest, false},
{http.StatusUnauthorized, false},
{http.StatusTooManyRequests, true}, // 429
{http.StatusInternalServerError, false},
{http.StatusBadGateway, false},
{http.StatusServiceUnavailable, true}, // 503
{http.StatusGatewayTimeout, true}, // 504
}
for _, tt := range tests {
assert.Equal(t, tt.isFailure, circuitbreaker.DefaultIsFailure(tt.statusCode), "status code %d", tt.statusCode)
}
}
func TestStateToGaugeValue(t *testing.T) {
t.Parallel()
assert.Equal(t, float64(0), circuitbreaker.StateToGaugeValue(gobreaker.StateClosed))
assert.Equal(t, float64(0.5), circuitbreaker.StateToGaugeValue(gobreaker.StateHalfOpen))
assert.Equal(t, float64(1), circuitbreaker.StateToGaugeValue(gobreaker.StateOpen))
}
+60
View File
@@ -0,0 +1,60 @@
package aibridge
import (
"net/http"
"strings"
)
type Client string
const (
// Possible values for the "client" field in interception records.
// Must be kept in sync with documentation: https://github.com/coder/coder/blob/90c11f3386578da053ec5cd9f1475835b980e7c7/docs/ai-coder/ai-bridge/monitoring.md?plain=1#L36-L44
ClientClaudeCode Client = "Claude Code"
ClientCodex Client = "Codex"
ClientZed Client = "Zed"
ClientCopilotVSC Client = "GitHub Copilot (VS Code)"
ClientCopilotCLI Client = "GitHub Copilot (CLI)"
ClientKilo Client = "Kilo Code"
ClientCoderAgents Client = "Coder Agents"
ClientCrush Client = "Charm Crush"
ClientMux Client = "Mux"
ClientRoo Client = "Roo Code"
ClientCursor Client = "Cursor"
ClientUnknown Client = "Unknown"
)
// GuessClient attempts to guess the client application from the request headers.
// Not all clients set proper user agent headers, so this is a best-effort approach.
// Based on https://github.com/coder/aibridge/issues/20#issuecomment-3769444101.
func GuessClient(r *http.Request) Client {
userAgent := strings.ToLower(r.UserAgent())
originator := r.Header.Get("originator")
// Must be kept in sync with documentation: https://github.com/coder/coder/blob/90c11f3386578da053ec5cd9f1475835b980e7c7/docs/ai-coder/ai-bridge/monitoring.md?plain=1#L36-L44
switch {
case strings.HasPrefix(userAgent, "mux/"):
return ClientMux
case strings.HasPrefix(userAgent, "claude"):
return ClientClaudeCode
case strings.HasPrefix(userAgent, "codex"):
return ClientCodex
case strings.HasPrefix(userAgent, "zed/"):
return ClientZed
case strings.HasPrefix(userAgent, "githubcopilotchat/"):
return ClientCopilotVSC
case strings.HasPrefix(userAgent, "copilot/"):
return ClientCopilotCLI
case strings.HasPrefix(userAgent, "kilo-code/") || originator == "kilo-code":
return ClientKilo
case strings.HasPrefix(userAgent, "roo-code/") || originator == "roo-code":
return ClientRoo
case strings.HasPrefix(userAgent, "coder-agents/"):
return ClientCoderAgents
case strings.HasPrefix(userAgent, "charm crush/"):
return ClientCrush
case r.Header.Get("x-cursor-client-version") != "":
return ClientCursor
}
return ClientUnknown
}
+125
View File
@@ -0,0 +1,125 @@
package aibridge_test
import (
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/aibridge"
)
func TestGuessClient(t *testing.T) {
t.Parallel()
tests := []struct {
name string
userAgent string
headers map[string]string
wantClient aibridge.Client
}{
{
name: "mux",
userAgent: "mux/0.19.0-next.2.gcceff159 ai-sdk/openai/3.0.36 ai-sdk/provider-utils/4.0.15 runtime/node.js/22",
wantClient: aibridge.ClientMux,
},
{
name: "claude_code",
userAgent: "claude-cli/2.0.67 (external, cli)",
wantClient: aibridge.ClientClaudeCode,
},
{
name: "codex_cli",
userAgent: "codex_cli_rs/0.87.0 (Mac OS 26.2.0; arm64) ghostty/1.3.0-main_250877ef",
wantClient: aibridge.ClientCodex,
},
{
name: "zed",
userAgent: "Zed/0.219.4+stable.119.abc123 (macos; aarch64)",
wantClient: aibridge.ClientZed,
},
{
name: "github_copilot_vsc",
userAgent: "GitHubCopilotChat/0.37.2026011603",
wantClient: aibridge.ClientCopilotVSC,
},
{
name: "github_copilot_cli",
userAgent: "copilot/0.0.403 (client/cli linux v24.11.1)",
wantClient: aibridge.ClientCopilotCLI,
},
{
name: "kilo_code_user_agent",
userAgent: "kilo-code/5.1.0 (darwin 25.2.0; arm64) node/22.21.1",
wantClient: aibridge.ClientKilo,
},
{
name: "kilo_code_originator",
headers: map[string]string{"Originator": "kilo-code"},
wantClient: aibridge.ClientKilo,
},
{
name: "roo_code_user_agent",
userAgent: "roo-code/3.45.0 (darwin 25.2.0; arm64) node/22.21.1",
wantClient: aibridge.ClientRoo,
},
{
name: "roo_code_originator",
headers: map[string]string{"Originator": "roo-code"},
wantClient: aibridge.ClientRoo,
},
{
name: "coder_agents",
userAgent: "coder-agents/v2.24.0 (linux/amd64)",
wantClient: aibridge.ClientCoderAgents,
},
{
name: "coder_agents_dev",
userAgent: "coder-agents/v0.0.0-devel (darwin/arm64)",
wantClient: aibridge.ClientCoderAgents,
},
{
name: "charm_crush",
userAgent: "Charm Crush/0.1.11",
wantClient: aibridge.ClientCrush,
},
{
name: "cursor_x_cursor_client_version",
userAgent: "connect-es/1.6.1",
headers: map[string]string{"X-Cursor-client-version": "0.50.0"},
wantClient: aibridge.ClientCursor,
},
{
name: "cursor_x_cursor_some_other_header",
headers: map[string]string{"x-cursor-client-version": "abc123"},
wantClient: aibridge.ClientCursor,
},
{
name: "unknown_client",
userAgent: "ccclaude-cli/calude-with-wrong-prefix",
wantClient: aibridge.ClientUnknown,
},
{
name: "empty_user_agent",
userAgent: "",
wantClient: aibridge.ClientUnknown,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "", nil)
require.NoError(t, err)
req.Header.Set("User-Agent", tt.userAgent)
for key, value := range tt.headers {
req.Header.Set(key, value)
}
got := aibridge.GuessClient(req)
require.Equal(t, tt.wantClient, got)
})
}
}
+93
View File
@@ -0,0 +1,93 @@
package config
import "time"
const (
ProviderAnthropic = "anthropic"
ProviderOpenAI = "openai"
ProviderCopilot = "copilot"
)
type Anthropic struct {
// Name is the provider instance name. If empty, defaults to "anthropic".
Name string
BaseURL string
Key string
APIDumpDir string
CircuitBreaker *CircuitBreaker
SendActorHeaders bool
ExtraHeaders map[string]string
// BYOKBearerToken is set in BYOK mode when the user authenticates
// with a access token. When set, the access token is used for upstream
// LLM requests instead of the API key.
BYOKBearerToken string
// MaxRetries controls the number of automatic retries the SDK will perform
// on transient errors. If nil, the SDK default (2) is used.
// Set to 0 to disable retries entirely.
MaxRetries *int
}
type AWSBedrock struct {
Region string
AccessKey, AccessKeySecret string
Model, SmallFastModel string
// If set, requests will be sent to this URL instead of the default AWS Bedrock endpoint
// (https://bedrock-runtime.{region}.amazonaws.com).
// This is useful for routing requests through a proxy or for testing.
BaseURL string
}
type OpenAI struct {
// Name is the provider instance name. If empty, defaults to "openai".
Name string
BaseURL string
Key string
APIDumpDir string
CircuitBreaker *CircuitBreaker
SendActorHeaders bool
ExtraHeaders map[string]string
// MaxRetries controls the number of automatic retries the SDK will perform
// on transient errors. If nil, the SDK default (2) is used.
// Set to 0 to disable retries entirely.
MaxRetries *int
}
type Copilot struct {
// Name is the provider instance name. If empty, defaults to "copilot".
Name string
BaseURL string
APIDumpDir string
CircuitBreaker *CircuitBreaker
// MaxRetries controls the number of automatic retries the SDK will perform
// on transient errors. If nil, the SDK default (2) is used.
// Set to 0 to disable retries entirely.
MaxRetries *int
}
// CircuitBreaker holds configuration for circuit breakers.
type CircuitBreaker struct {
// MaxRequests is the maximum number of requests allowed in half-open state.
MaxRequests uint32
// Interval is the cyclic period of the closed state for clearing internal counts.
Interval time.Duration
// Timeout is how long the circuit stays open before transitioning to half-open.
Timeout time.Duration
// FailureThreshold is the number of consecutive failures that triggers the circuit to open.
FailureThreshold uint32
// IsFailure determines if a status code should count as a failure.
// If nil, defaults to DefaultIsFailure.
IsFailure func(statusCode int) bool
// OpenErrorResponse returns the response body when the circuit is open.
// This should match the provider's error format.
OpenErrorResponse func() []byte
}
// DefaultCircuitBreaker returns sensible defaults for circuit breaker configuration.
func DefaultCircuitBreaker() CircuitBreaker {
return CircuitBreaker{
FailureThreshold: 5,
Interval: 10 * time.Second,
Timeout: 30 * time.Second,
MaxRequests: 3,
}
}
+38
View File
@@ -0,0 +1,38 @@
package context
import (
"context"
"github.com/coder/coder/v2/aibridge/recorder"
)
type (
actorContextKey struct{}
)
type Actor struct {
ID string
Metadata recorder.Metadata
}
func AsActor(ctx context.Context, actorID string, metadata recorder.Metadata) context.Context {
return context.WithValue(ctx, actorContextKey{}, &Actor{ID: actorID, Metadata: metadata})
}
func ActorFromContext(ctx context.Context) *Actor {
a, ok := ctx.Value(actorContextKey{}).(*Actor)
if !ok {
return nil
}
return a
}
// ActorIDFromContext safely extracts the actor ID from the context.
// Returns an empty string if no actor is found.
func ActorIDFromContext(ctx context.Context) string {
if actor := ActorFromContext(ctx); actor != nil {
return actor.ID
}
return ""
}
+89
View File
@@ -0,0 +1,89 @@
package context_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/recorder"
)
func TestAsActor(t *testing.T) {
t.Parallel()
// Given: a metadata map
metadata := recorder.Metadata{"key": "value"}
// When: storing an actor in the context
ctx := aibcontext.AsActor(context.Background(), "actor-123", metadata)
// Then: the actor should be retrievable with correct ID and metadata
actor := aibcontext.ActorFromContext(ctx)
require.NotNil(t, actor)
assert.Equal(t, "actor-123", actor.ID)
assert.Equal(t, "value", actor.Metadata["key"])
}
func TestActorFromContext(t *testing.T) {
t.Parallel()
t.Run("returns actor when present", func(t *testing.T) {
t.Parallel()
// Given: a context with an actor
ctx := aibcontext.AsActor(context.Background(), "test-id", recorder.Metadata{})
// When: extracting the actor from context
actor := aibcontext.ActorFromContext(ctx)
// Then: the actor should be returned with correct ID
require.NotNil(t, actor)
assert.Equal(t, "test-id", actor.ID)
})
t.Run("returns nil when no actor", func(t *testing.T) {
t.Parallel()
// Given: a context without an actor
ctx := context.Background()
// When: extracting the actor from context
actor := aibcontext.ActorFromContext(ctx)
// Then: nil should be returned
assert.Nil(t, actor)
})
}
func TestActorIDFromContext(t *testing.T) {
t.Parallel()
t.Run("returns actor ID when present", func(t *testing.T) {
t.Parallel()
// Given: a context with an actor
ctx := aibcontext.AsActor(context.Background(), "test-actor-id", recorder.Metadata{})
// When: extracting the actor ID from context
got := aibcontext.ActorIDFromContext(ctx)
// Then: the actor ID should be returned
assert.Equal(t, "test-actor-id", got)
})
t.Run("returns empty string when no actor", func(t *testing.T) {
t.Parallel()
// Given: a context without an actor
ctx := context.Background()
// When: extracting the actor ID from context
got := aibcontext.ActorIDFromContext(ctx)
// Then: an empty string should be returned
assert.Empty(t, got)
})
}
+25
View File
@@ -0,0 +1,25 @@
These fixtures were created by adding logging middleware to API calls to view the raw requests/responses.
```go
...
opts = append(opts, option.WithMiddleware(LoggingMiddleware))
...
func LoggingMiddleware(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) {
reqOut, _ := httputil.DumpRequest(req, true)
// Forward the request to the next handler
res, err = next(req)
fmt.Printf("[req] %s\n", reqOut)
// Handle stuff after the request
if err != nil {
return res, err
}
respOut, _ := httputil.DumpResponse(res, true)
fmt.Printf("[resp] %s\n", respOut)
return res, err
}
```
@@ -0,0 +1,64 @@
API endpoints not explicitly handled will fallthrough to upstream via reverse-proxy.
-- non-streaming --
{
"data": [
{
"type": "model",
"id": "claude-opus-4-1-20250805",
"display_name": "Claude Opus 4.1",
"created_at": "2025-08-05T00:00:00Z"
},
{
"type": "model",
"id": "claude-opus-4-20250514",
"display_name": "Claude Opus 4",
"created_at": "2025-05-22T00:00:00Z"
},
{
"type": "model",
"id": "claude-sonnet-4-20250514",
"display_name": "Claude Sonnet 4",
"created_at": "2025-05-22T00:00:00Z"
},
{
"type": "model",
"id": "claude-3-7-sonnet-20250219",
"display_name": "Claude Sonnet 3.7",
"created_at": "2025-02-24T00:00:00Z"
},
{
"type": "model",
"id": "claude-3-5-sonnet-20241022",
"display_name": "Claude Sonnet 3.5 (New)",
"created_at": "2024-10-22T00:00:00Z"
},
{
"type": "model",
"id": "claude-3-5-haiku-20241022",
"display_name": "Claude Haiku 3.5",
"created_at": "2024-10-22T00:00:00Z"
},
{
"type": "model",
"id": "claude-3-5-sonnet-20240620",
"display_name": "Claude Sonnet 3.5 (Old)",
"created_at": "2024-06-20T00:00:00Z"
},
{
"type": "model",
"id": "claude-3-haiku-20240307",
"display_name": "Claude Haiku 3",
"created_at": "2024-03-07T00:00:00Z"
},
{
"type": "model",
"id": "claude-3-opus-20240229",
"display_name": "Claude Opus 3",
"created_at": "2024-02-29T00:00:00Z"
}
],
"has_more": false,
"first_id": "claude-opus-4-1-20250805",
"last_id": "claude-3-opus-20240229"
}
@@ -0,0 +1,155 @@
Simple request using a Haiku model (small/fast model).
Used to validate that prompts are captured for small/fast models like Haiku,
which Claude Code uses for ancillary tasks (e.g. generating session titles,
push notification summaries).
-- request --
{
"max_tokens": 8192,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "how many angels can dance on the head of a pin\n"
}
]
}
],
"model": "claude-haiku-4-5",
"temperature": 1
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_01Pvyf26bY17RcjmWfJsXGBn","type":"message","role":"assistant","model":"claude-haiku-4-5-20251001","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"This is a classic philosophical question about medieval scholasticism. I'll give a thoughtful answer."}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""} }
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"This"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" is a famous philosophical question often used to illustrate medieval"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" scholastic debates that seem pointless or ov"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"erly abstract. The question \"How many angels can dance on the head of"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" a pin?\" is typically cited as an example of us"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"eless speculation.\n\nHistorically, medieval theolog"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"ians did debate the nature of angels -"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" whether they were incorporeal beings, how"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" they occupied space, and whether multiple angels could exist"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" in the same location. However, there"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"'s little evidence they literally"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" debated dancing angels on pinheads.\n\nThe question has"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" no factual answer since it depends on assumptions about:"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"\n- The existence and nature of angels\n- Whether"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" incorporeal beings occupy physical space\n- What"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" constitutes \"dancing\" for a spiritual"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" entity\n- The size of both the"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" pin and the angels\n\nIt's become a metaph"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"or for overthinking trivial matters"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" or getting lost in theoretical discussions disconnected from practical reality."} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" Some use it to critique certain types of academic"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" or theological debate, while others defen"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"d the value of exploring fundamental questions about existence an"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"d metaphysics.\n\nSo while u"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"nanswerable literally, it serves as an interesting lens"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" for discussing the nature of philosophical inquiry itself."} }
event: content_block_stop
data: {"type":"content_block_stop","index":1 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":240} }
event: message_stop
data: {"type":"message_stop" }
-- non-streaming --
{
"id": "msg_01Pvyf26bY17RcjmWfJsXGBn",
"type": "message",
"role": "assistant",
"model": "claude-haiku-4-5-20251001",
"content": [
{
"type": "thinking",
"thinking": "This is a classic philosophical question about medieval scholasticism. I'll give a thoughtful answer."
},
{
"type": "text",
"text": "This is a famous philosophical question, often called \"How many angels can dance on the head of a pin?\" It's typically used to represent pointless or overly abstract theological debates.\n\nThe question doesn't have a literal answer because:\n\n1. **Historical context**: It's often attributed to medieval scholastic philosophers, though there's little evidence they actually debated this exact question. It became a popular way to mock what some saw as useless academic arguments.\n\n2. **Philosophical purpose**: The question highlights the difficulty of discussing non-physical beings (angels) in physical terms (space on a pinhead).\n\n3. **Different interpretations**: \n - If angels are purely spiritual, they might not take up physical space at all\n - If they do occupy space, we'd need to know their \"size\"\n - The question might be asking about the nature of space, matter, and spirit\n\nSo the real answer is that it's not meant to be answered literally - it's a thought experiment about the limits of rational inquiry and the sometimes absurd directions theological speculation can take.\n\nWould you like to explore the philosophical implications behind this question, or were you thinking about it in a different context?"
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 18,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"output_tokens": 254,
"service_tier": "standard"
}
}
@@ -0,0 +1,152 @@
Claude Code has builtin tools to (e.g.) explore the filesystem.
This fixture has two thinking blocks before the tool_use block.
-- request --
{
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"tools": [
{
"name": "Read",
"description": "Read the contents of a file at the given path.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The absolute path to the file to read"
}
},
"required": ["file_path"]
}
}
],
"messages": [
{
"role": "user",
"content": "read the foo file"
}
]
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_015SQewixvT9s4cABCVvUE6g","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":22,"cache_read_input_tokens":13993,"output_tokens":5,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"The user wants me to read a file called \"foo\". Let me find and read it."}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"Eu8BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"thinking","thinking":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"thinking_delta","thinking":"I should use the Read tool to access the file contents."}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"signature_delta","signature":"Aa1BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="}}
event: content_block_stop
data: {"type":"content_block_stop","index":1}
event: content_block_start
data: {"type":"content_block_start","index":2,"content_block":{"type":"tool_use","id":"toolu_01RX68weRSquLx6HUTj65iBo","name":"Read","input":{}}}
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":""} }
event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"{\"file_path\": \"/tmp/blah/foo"} }
event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"\"}"} }
event: content_block_stop
data: {"type":"content_block_stop","index":2 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":61} }
event: message_stop
data: {"type":"message_stop" }
-- non-streaming --
{
"id": "msg_01JHKqEmh7wYuPXqUWUvusfL",
"container": {
"id": "",
"expires_at": "0001-01-01T00:00:00Z"
},
"content": [
{
"type": "thinking",
"thinking": "The user wants me to read a file called \"foo\". Let me find and read it.",
"signature": "Eu8BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="
},
{
"type": "thinking",
"thinking": "I should use the Read tool to access the file contents.",
"signature": "Aa1BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="
},
{
"citations": null,
"text": "",
"type": "tool_use",
"id": "toolu_01AusGgY5aKFhzWrFBv9JfHq",
"input": {
"file_path": "/tmp/blah/foo"
},
"name": "Read",
"content": {
"OfWebSearchResultBlockArray": null,
"OfString": "",
"OfMCPToolResultBlockContent": null,
"error_code": "",
"type": "",
"content": null,
"return_code": 0,
"stderr": "",
"stdout": ""
},
"tool_use_id": "",
"server_name": "",
"is_error": false,
"file_id": "",
"signature": "",
"thinking": "",
"data": ""
}
],
"model": "claude-sonnet-4-20250514",
"role": "assistant",
"stop_reason": "tool_use",
"stop_sequence": "",
"type": "message",
"usage": {
"cache_creation": {
"ephemeral_1h_input_tokens": 0,
"ephemeral_5m_input_tokens": 0
},
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 23490,
"input_tokens": 5,
"output_tokens": 84,
"server_tool_use": {
"web_search_requests": 0
},
"service_tier": "standard"
}
}
@@ -0,0 +1,35 @@
Simple request + error which occurs before streaming begins (where applicable).
-- request --
{
"max_tokens": 8192,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "yo"
}
]
}
],
"model": "claude-sonnet-4-0",
"temperature": 1
}
-- streaming --
HTTP/2.0 400 Bad Request
Content-Length: 164
Content-Type: application/json
{"type":"error","error":{"type":"invalid_request_error","message":"prompt is too long: 205429 tokens > 200000 maximum"},"request_id":"req_011CV5Jab6gR3ZNs9Sj6apiD"}
-- non-streaming --
HTTP/2.0 400 Bad Request
Content-Length: 164
Content-Type: application/json
{"type":"error","error":{"type":"invalid_request_error","message":"prompt is too long: 205429 tokens > 200000 maximum"},"request_id":"req_011CV5Jab6gR3ZNs9Sj6apiD"}
+152
View File
@@ -0,0 +1,152 @@
Simple request.
-- request --
{
"max_tokens": 8192,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "how many angels can dance on the head of a pin\n"
}
]
}
],
"model": "claude-sonnet-4-0",
"temperature": 1
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_01Pvyf26bY17RcjmWfJsXGBn","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"This is a classic philosophical question about medieval scholasticism. I'll give a thoughtful answer."}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""} }
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"This"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" is a famous philosophical question often used to illustrate medieval"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" scholastic debates that seem pointless or ov"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"erly abstract. The question \"How many angels can dance on the head of"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" a pin?\" is typically cited as an example of us"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"eless speculation.\n\nHistorically, medieval theolog"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"ians did debate the nature of angels -"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" whether they were incorporeal beings, how"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" they occupied space, and whether multiple angels could exist"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" in the same location. However, there"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"'s little evidence they literally"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" debated dancing angels on pinheads.\n\nThe question has"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" no factual answer since it depends on assumptions about:"}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"\n- The existence and nature of angels\n- Whether"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" incorporeal beings occupy physical space\n- What"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" constitutes \"dancing\" for a spiritual"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" entity\n- The size of both the"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" pin and the angels\n\nIt's become a metaph"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"or for overthinking trivial matters"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" or getting lost in theoretical discussions disconnected from practical reality."} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" Some use it to critique certain types of academic"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" or theological debate, while others defen"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"d the value of exploring fundamental questions about existence an"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"d metaphysics.\n\nSo while u"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"nanswerable literally, it serves as an interesting lens"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":" for discussing the nature of philosophical inquiry itself."} }
event: content_block_stop
data: {"type":"content_block_stop","index":1 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":240} }
event: message_stop
data: {"type":"message_stop" }
-- non-streaming --
{
"id": "msg_01Pvyf26bY17RcjmWfJsXGBn",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-20250514",
"content": [
{
"type": "thinking",
"thinking": "This is a classic philosophical question about medieval scholasticism. I'll give a thoughtful answer."
},
{
"type": "text",
"text": "This is a famous philosophical question, often called \"How many angels can dance on the head of a pin?\" It's typically used to represent pointless or overly abstract theological debates.\n\nThe question doesn't have a literal answer because:\n\n1. **Historical context**: It's often attributed to medieval scholastic philosophers, though there's little evidence they actually debated this exact question. It became a popular way to mock what some saw as useless academic arguments.\n\n2. **Philosophical purpose**: The question highlights the difficulty of discussing non-physical beings (angels) in physical terms (space on a pinhead).\n\n3. **Different interpretations**: \n - If angels are purely spiritual, they might not take up physical space at all\n - If they do occupy space, we'd need to know their \"size\"\n - The question might be asking about the nature of space, matter, and spirit\n\nSo the real answer is that it's not meant to be answered literally - it's a thought experiment about the limits of rational inquiry and the sometimes absurd directions theological speculation can take.\n\nWould you like to explore the philosophical implications behind this question, or were you thinking about it in a different context?"
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 18,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"output_tokens": 254,
"service_tier": "standard"
}
}
@@ -0,0 +1,51 @@
Simple Bedrock request. Tests that fields unsupported by Bedrock are removed
and adaptive thinking is converted to enabled with a budget. Includes all
bedrockUnsupportedFields (metadata, service_tier, container, inference_geo)
and beta-gated fields (output_config, context_management).
-- request --
{
"model": "claude-sonnet-4-6",
"max_tokens": 32000,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Hello."
}
]
}
],
"thinking": {"type": "adaptive"},
"metadata": {"user_id": "session_abc123"},
"service_tier": "auto",
"container": {"type": "ephemeral"},
"inference_geo": {"allow": ["us"]},
"output_config": {"effort": "medium"},
"context_management": {"edits": [{"type": "clear_thinking_20251015", "keep": "all"}]},
"stream": true
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_bdrk_01Test","type":"message","role":"assistant","model":"claude-sonnet-4-5-20250929","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4}}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello! How can I help?"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":10}}
event: message_stop
data: {"type":"message_stop"}
-- non-streaming --
{"id":"msg_bdrk_01Test","type":"message","role":"assistant","model":"claude-sonnet-4-5-20250929","content":[{"type":"text","text":"Hello! How can I help?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10}}
@@ -0,0 +1,181 @@
Claude Code has builtin tools to (e.g.) explore the filesystem.
-- request --
{
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"tools": [
{
"name": "Read",
"description": "Read the contents of a file at the given path.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The absolute path to the file to read"
}
},
"required": ["file_path"]
}
}
],
"messages": [
{
"role": "user",
"content": "read the foo file"
}
]
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_015SQewixvT9s4cABCVvUE6g","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":22,"cache_read_input_tokens":13993,"output_tokens":5,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"The user wants me to read"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" a"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" file called \""} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"foo\"."} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" Let me find"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" and"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" read it."} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"Eu8BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01RX68weRSquLx6HUTj65iBo","name":"Read","input":{}}}
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"file_path\": \"/tmp/blah/foo"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\"}"} }
event: content_block_stop
data: {"type":"content_block_stop","index":1 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":61} }
event: message_stop
data: {"type":"message_stop" }
-- non-streaming --
{
"id": "msg_01JHKqEmh7wYuPXqUWUvusfL",
"container": {
"id": "",
"expires_at": "0001-01-01T00:00:00Z"
},
"content": [
{
"type": "thinking",
"thinking": "The user wants me to read a file called \"foo\". Let me find and read it.",
"signature": "Eu8BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="
},
{
"citations": null,
"text": "I can see there's a file named `foo` in the `/tmp/blah` directory. Let me read it.",
"type": "text",
"id": "",
"input": null,
"name": "",
"content": {
"OfWebSearchResultBlockArray": null,
"OfString": "",
"OfMCPToolResultBlockContent": null,
"error_code": "",
"type": "",
"content": null,
"return_code": 0,
"stderr": "",
"stdout": ""
},
"tool_use_id": "",
"server_name": "",
"is_error": false,
"file_id": "",
"signature": "",
"thinking": "",
"data": ""
},
{
"citations": null,
"text": "",
"type": "tool_use",
"id": "toolu_01AusGgY5aKFhzWrFBv9JfHq",
"input": {
"file_path": "/tmp/blah/foo"
},
"name": "Read",
"content": {
"OfWebSearchResultBlockArray": null,
"OfString": "",
"OfMCPToolResultBlockContent": null,
"error_code": "",
"type": "",
"content": null,
"return_code": 0,
"stderr": "",
"stdout": ""
},
"tool_use_id": "",
"server_name": "",
"is_error": false,
"file_id": "",
"signature": "",
"thinking": "",
"data": ""
}
],
"model": "claude-sonnet-4-20250514",
"role": "assistant",
"stop_reason": "tool_use",
"stop_sequence": "",
"type": "message",
"usage": {
"cache_creation": {
"ephemeral_1h_input_tokens": 0,
"ephemeral_5m_input_tokens": 0
},
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 23490,
"input_tokens": 5,
"output_tokens": 84,
"server_tool_use": {
"web_search_requests": 0
},
"service_tier": "standard"
}
}
@@ -0,0 +1,175 @@
Claude Code has builtin tools to (e.g.) explore the filesystem.
This fixture has a single thinking block followed by two parallel tool_use blocks.
The thinking should only be attributed to the first tool_use.
-- request --
{
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"tools": [
{
"name": "Read",
"description": "Read the contents of a file at the given path.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The absolute path to the file to read"
}
},
"required": ["file_path"]
}
}
],
"messages": [
{
"role": "user",
"content": "read the foo and bar files"
}
]
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_01ParallelToolStream","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":22,"cache_read_input_tokens":13993,"output_tokens":5,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"The user wants me to read two files: \"foo\" and \"bar\". I'll read both of them."}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"Eu8BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01ParallelFirst000000000","name":"Read","input":{}}}
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"file_path\": \"/tmp/blah/foo"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\"}"} }
event: content_block_stop
data: {"type":"content_block_stop","index":1 }
event: content_block_start
data: {"type":"content_block_start","index":2,"content_block":{"type":"tool_use","id":"toolu_01ParallelSecond00000000","name":"Read","input":{}}}
event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":""} }
event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"{\"file_path\": \"/tmp/blah/bar"} }
event: content_block_delta
data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"\"}"} }
event: content_block_stop
data: {"type":"content_block_stop","index":2 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":72} }
event: message_stop
data: {"type":"message_stop" }
-- non-streaming --
{
"id": "msg_01ParallelToolBlocking",
"container": {
"id": "",
"expires_at": "0001-01-01T00:00:00Z"
},
"content": [
{
"type": "thinking",
"thinking": "The user wants me to read two files: \"foo\" and \"bar\". I'll read both of them.",
"signature": "Eu8BCkYICxgCKkBR++kFr7Za2JhF/9OCpjEc46/EcipL75RK+MEbxJ/VBJPWQTWrNGfwb5khWYJtKEpjjkH07cR/MQvThfb7t7CkEgwU4pKwL7NuZXd1/wgaDILyd0bYMqQovWo3dyIw95Ny7yZPljNBDLsvMBdBr7w+RtbU+AlSftjBuBZHp0VzI54/W+9u6f7qfx0JXsVBKldqqOjFvewT8Xm6Qp/77g6/j0zBiuAQABj/6vS1qATjd8KSIFDg9G/tCtzwmV/T/egmzswWd5CBiAhW6lgJgEDRr+gRUrFSOB7o3hypW8FUnUrr1JtzzwMYAQ=="
},
{
"citations": null,
"text": "",
"type": "tool_use",
"id": "toolu_01ParallelBlockFirst0000",
"input": {
"file_path": "/tmp/blah/foo"
},
"name": "Read",
"content": {
"OfWebSearchResultBlockArray": null,
"OfString": "",
"OfMCPToolResultBlockContent": null,
"error_code": "",
"type": "",
"content": null,
"return_code": 0,
"stderr": "",
"stdout": ""
},
"tool_use_id": "",
"server_name": "",
"is_error": false,
"file_id": "",
"signature": "",
"thinking": "",
"data": ""
},
{
"citations": null,
"text": "",
"type": "tool_use",
"id": "toolu_01ParallelBlockSecond000",
"input": {
"file_path": "/tmp/blah/bar"
},
"name": "Read",
"content": {
"OfWebSearchResultBlockArray": null,
"OfString": "",
"OfMCPToolResultBlockContent": null,
"error_code": "",
"type": "",
"content": null,
"return_code": 0,
"stderr": "",
"stdout": ""
},
"tool_use_id": "",
"server_name": "",
"is_error": false,
"file_id": "",
"signature": "",
"thinking": "",
"data": ""
}
],
"model": "claude-sonnet-4-20250514",
"role": "assistant",
"stop_reason": "tool_use",
"stop_sequence": "",
"type": "message",
"usage": {
"cache_creation": {
"ephemeral_1h_input_tokens": 0,
"ephemeral_5m_input_tokens": 0
},
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 23490,
"input_tokens": 5,
"output_tokens": 95,
"server_tool_use": {
"web_search_requests": 0
},
"service_tier": "standard"
}
}
@@ -0,0 +1,163 @@
Coder MCP tools automatically injected.
-- request --
{
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"messages": [
{
"role": "user",
"content": "list coder workspace IDs for admin"
}
]
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_01JWGa2JHsKBHL28Cjr2dvPK","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7545,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I'll list the work"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"spaces for the admin user to get their"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" workspace IDs."} }
event: content_block_stop
data: {"type":"content_block_stop","index":0 }
event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01TSQLR6R6wBUqoxGPjQKDAj","name":"bmcp_coder_coder_list_workspaces","input":{}} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"owner\""} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":": \"ad"} }
event: content_block_delta
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"min\"}"} }
event: content_block_stop
data: {"type":"content_block_stop","index":1 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":74}}
event: message_stop
data: {"type":"message_stop" }
-- streaming/tool-call --
event: message_start
data: {"type":"message_start","message":{"id":"msg_01LZSVzMCLivzXrp6ZnTcmeG","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":7763,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Here"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" are the workspace IDs for the admin user:\n\n**"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Admin's Workspaces:**\n- Workspace ID: `dd711d5c-83c"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"6-4c08-a0af-b73055906e8"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"c`\n - Name: `bob`\n - Template: `docker"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"`\n - Template ID: `b3a9d9b4-486a-4"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"f21-8884-d81d5dbdd837`"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"\n\nThe admin user currently has 1 workspace named \"bob\" created from"} }
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the \"docker\" template."} }
event: content_block_stop
data: {"type":"content_block_stop","index":0 }
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":128} }
event: message_stop
data: {"type":"message_stop" }
-- non-streaming --
{
"id": "msg_01FwkWU26guw9EwkL8zeacPL",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-20250514",
"content": [
{
"type": "text",
"text": "I'll list the workspaces for the admin user to get their workspace IDs."
},
{
"type": "tool_use",
"id": "toolu_01QjNz5b3HxAqAccTVnSMsKP",
"name": "bmcp_coder_coder_list_workspaces",
"input": {
"owner": "admin"
}
}
],
"stop_reason": "tool_use",
"stop_sequence": null,
"usage": {
"input_tokens": 7545,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"output_tokens": 75,
"service_tier": "standard"
}
}
-- non-streaming/tool-call --
{
"id": "msg_01Sr5BnPSwodTo8Df4XvUBg5",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-20250514",
"content": [
{
"type": "text",
"text": "Here are the Coder workspace IDs for the admin user:\n\n**Workspace ID:** `dd711d5c-83c6-4c08-a0af-b73055906e8c`\n- **Name:** bob\n- **Template:** docker\n- **Template ID:** b3a9d9b4-486a-4f21-8884-d81d5dbdd837\n- **Status:** Up to date (not outdated)\n\nThe admin user currently has 1 workspace named \"bob\" running on the \"docker\" template."
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 7763,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"output_tokens": 129,
"service_tier": "standard"
}
}
@@ -0,0 +1,34 @@
Simple request + error.
-- request --
{
"max_tokens": 8192,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "yo"
}
]
}
],
"model": "claude-sonnet-4-0",
"temperature": 1,
"stream": true
}
-- streaming --
event: message_start
data: {"type":"message_start","message":{"id":"msg_01Pvyf26bY17RcjmWfJsXGBn","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} }
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }
event: ping
data: {"type": "ping"}
event: error
data: {"type": "error", "error": {"type": "api_error", "message": "Overloaded"}}
+247
View File
@@ -0,0 +1,247 @@
package fixtures
import (
_ "embed"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/tools/txtar"
)
var (
//go:embed anthropic/simple.txtar
AntSimple []byte
//go:embed anthropic/single_builtin_tool.txtar
AntSingleBuiltinTool []byte
//go:embed anthropic/multi_thinking_builtin_tool.txtar
AntMultiThinkingBuiltinTool []byte
//go:embed anthropic/single_builtin_tool_parallel.txtar
AntSingleBuiltinToolParallel []byte
//go:embed anthropic/single_injected_tool.txtar
AntSingleInjectedTool []byte
//go:embed anthropic/fallthrough.txtar
AntFallthrough []byte
//go:embed anthropic/stream_error.txtar
AntMidStreamError []byte
//go:embed anthropic/non_stream_error.txtar
AntNonStreamError []byte
//go:embed anthropic/simple_bedrock.txtar
AntSimpleBedrock []byte
//go:embed anthropic/haiku_simple.txtar
AntHaikuSimple []byte
)
var (
//go:embed openai/chatcompletions/simple.txtar
OaiChatSimple []byte
//go:embed openai/chatcompletions/single_builtin_tool.txtar
OaiChatSingleBuiltinTool []byte
//go:embed openai/chatcompletions/single_injected_tool.txtar
OaiChatSingleInjectedTool []byte
//go:embed openai/chatcompletions/fallthrough.txtar
OaiChatFallthrough []byte
//go:embed openai/chatcompletions/stream_error.txtar
OaiChatMidStreamError []byte
//go:embed openai/chatcompletions/non_stream_error.txtar
OaiChatNonStreamError []byte
//go:embed openai/chatcompletions/streaming_injected_tool_no_preamble.txtar
OaiChatStreamingInjectedToolNoPreamble []byte
//go:embed openai/chatcompletions/streaming_injected_tool_nonzero_index.txtar
OaiChatStreamingInjectedToolNonzeroIndex []byte
)
var (
//go:embed openai/responses/blocking/simple.txtar
OaiResponsesBlockingSimple []byte
//go:embed openai/responses/blocking/single_builtin_tool.txtar
OaiResponsesBlockingSingleBuiltinTool []byte
//go:embed openai/responses/blocking/multi_reasoning_builtin_tool.txtar
OaiResponsesBlockingMultiReasoningBuiltinTool []byte
//go:embed openai/responses/blocking/commentary_builtin_tool.txtar
OaiResponsesBlockingCommentaryBuiltinTool []byte
//go:embed openai/responses/blocking/summary_and_commentary_builtin_tool.txtar
OaiResponsesBlockingSummaryAndCommentaryBuiltinTool []byte
//go:embed openai/responses/blocking/cached_input_tokens.txtar
OaiResponsesBlockingCachedInputTokens []byte
//go:embed openai/responses/blocking/custom_tool.txtar
OaiResponsesBlockingCustomTool []byte
//go:embed openai/responses/blocking/conversation.txtar
OaiResponsesBlockingConversation []byte
//go:embed openai/responses/blocking/http_error.txtar
OaiResponsesBlockingHTTPErr []byte
//go:embed openai/responses/blocking/prev_response_id.txtar
OaiResponsesBlockingPrevResponseID []byte
//go:embed openai/responses/blocking/single_builtin_tool_parallel.txtar
OaiResponsesBlockingSingleBuiltinToolParallel []byte
//go:embed openai/responses/blocking/single_injected_tool.txtar
OaiResponsesBlockingSingleInjectedTool []byte
//go:embed openai/responses/blocking/single_injected_tool_error.txtar
OaiResponsesBlockingSingleInjectedToolError []byte
//go:embed openai/responses/blocking/wrong_response_format.txtar
OaiResponsesBlockingWrongResponseFormat []byte
)
var (
//go:embed openai/responses/streaming/simple.txtar
OaiResponsesStreamingSimple []byte
//go:embed openai/responses/streaming/codex_example.txtar
OaiResponsesStreamingCodex []byte
//go:embed openai/responses/streaming/builtin_tool.txtar
OaiResponsesStreamingBuiltinTool []byte
//go:embed openai/responses/streaming/multi_reasoning_builtin_tool.txtar
OaiResponsesStreamingMultiReasoningBuiltinTool []byte
//go:embed openai/responses/streaming/commentary_builtin_tool.txtar
OaiResponsesStreamingCommentaryBuiltinTool []byte
//go:embed openai/responses/streaming/summary_and_commentary_builtin_tool.txtar
OaiResponsesStreamingSummaryAndCommentaryBuiltinTool []byte
//go:embed openai/responses/streaming/cached_input_tokens.txtar
OaiResponsesStreamingCachedInputTokens []byte
//go:embed openai/responses/streaming/custom_tool.txtar
OaiResponsesStreamingCustomTool []byte
//go:embed openai/responses/streaming/conversation.txtar
OaiResponsesStreamingConversation []byte
//go:embed openai/responses/streaming/http_error.txtar
OaiResponsesStreamingHTTPErr []byte
//go:embed openai/responses/streaming/prev_response_id.txtar
OaiResponsesStreamingPrevResponseID []byte
//go:embed openai/responses/streaming/single_builtin_tool_parallel.txtar
OaiResponsesStreamingSingleBuiltinToolParallel []byte
//go:embed openai/responses/streaming/single_injected_tool.txtar
OaiResponsesStreamingSingleInjectedTool []byte
//go:embed openai/responses/streaming/single_injected_tool_error.txtar
OaiResponsesStreamingSingleInjectedToolError []byte
//go:embed openai/responses/streaming/stream_error.txtar
OaiResponsesStreamingStreamError []byte
//go:embed openai/responses/streaming/stream_failure.txtar
OaiResponsesStreamingStreamFailure []byte
//go:embed openai/responses/streaming/wrong_response_format.txtar
OaiResponsesStreamingWrongResponseFormat []byte
)
// Section name constants matching the file names used in txtar fixtures.
const (
fileRequest = "request"
fileStreamingResponse = "streaming"
fileNonStreamingResponse = "non-streaming"
fileStreamingToolCall = "streaming/tool-call"
fileNonStreamingToolCall = "non-streaming/tool-call"
// Exported aliases so callers can check [Fixture.Has] before calling a
// getter that would otherwise fail the test.
SectionStreaming = fileStreamingResponse
SectionNonStreaming = fileNonStreamingResponse
SectionStreamingToolCall = fileStreamingToolCall
SectionNonStreamToolCall = fileNonStreamingToolCall
)
// Fixture holds the named sections of a parsed txtar test fixture.
type Fixture struct {
sections map[string][]byte
t *testing.T
}
// Has reports whether the fixture contains the named section.
func (f Fixture) Has(name string) bool {
_, ok := f.sections[name]
return ok
}
func (f Fixture) Request() []byte {
f.t.Helper()
v, ok := f.sections[fileRequest]
require.True(f.t, ok, "fixture archive missing %q section", fileRequest)
return v
}
func (f Fixture) Streaming() []byte {
f.t.Helper()
v, ok := f.sections[fileStreamingResponse]
require.True(f.t, ok, "fixture archive missing %q section", fileStreamingResponse)
return v
}
func (f Fixture) NonStreaming() []byte {
f.t.Helper()
v, ok := f.sections[fileNonStreamingResponse]
require.True(f.t, ok, "fixture archive missing %q section", fileNonStreamingResponse)
return v
}
func (f Fixture) StreamingToolCall() []byte {
f.t.Helper()
v, ok := f.sections[fileStreamingToolCall]
require.True(f.t, ok, "fixture archive missing %q section", fileStreamingToolCall)
return v
}
func (f Fixture) NonStreamingToolCall() []byte {
f.t.Helper()
v, ok := f.sections[fileNonStreamingToolCall]
require.True(f.t, ok, "fixture archive missing %q section", fileNonStreamingToolCall)
return v
}
// Parse parses raw txtar data into a [Fixture].
func Parse(t *testing.T, data []byte) Fixture {
t.Helper()
archive := txtar.Parse(data)
require.NotEmpty(t, archive.Files, "fixture archive has no files")
sections := make(map[string][]byte, len(archive.Files))
for _, f := range archive.Files {
sections[f.Name] = f.Data
}
return Fixture{sections: sections, t: t}
}
// Request extracts the "request" fixture from raw txtar data.
func Request(t *testing.T, fixture []byte) []byte {
t.Helper()
return Parse(t, fixture).Request()
}
@@ -0,0 +1,524 @@
API endpoints not explicitly handled will fallthrough to upstream via reverse-proxy.
-- non-streaming --
{
"object": "list",
"data": [
{
"id": "gpt-4-0613",
"object": "model",
"created": 1686588896,
"owned_by": "openai"
},
{
"id": "gpt-4",
"object": "model",
"created": 1687882411,
"owned_by": "openai"
},
{
"id": "gpt-3.5-turbo",
"object": "model",
"created": 1677610602,
"owned_by": "openai"
},
{
"id": "gpt-5-nano",
"object": "model",
"created": 1754426384,
"owned_by": "system"
},
{
"id": "gpt-5",
"object": "model",
"created": 1754425777,
"owned_by": "system"
},
{
"id": "gpt-5-mini-2025-08-07",
"object": "model",
"created": 1754425867,
"owned_by": "system"
},
{
"id": "gpt-5-mini",
"object": "model",
"created": 1754425928,
"owned_by": "system"
},
{
"id": "gpt-5-nano-2025-08-07",
"object": "model",
"created": 1754426303,
"owned_by": "system"
},
{
"id": "davinci-002",
"object": "model",
"created": 1692634301,
"owned_by": "system"
},
{
"id": "babbage-002",
"object": "model",
"created": 1692634615,
"owned_by": "system"
},
{
"id": "gpt-3.5-turbo-instruct",
"object": "model",
"created": 1692901427,
"owned_by": "system"
},
{
"id": "gpt-3.5-turbo-instruct-0914",
"object": "model",
"created": 1694122472,
"owned_by": "system"
},
{
"id": "dall-e-3",
"object": "model",
"created": 1698785189,
"owned_by": "system"
},
{
"id": "dall-e-2",
"object": "model",
"created": 1698798177,
"owned_by": "system"
},
{
"id": "gpt-4-1106-preview",
"object": "model",
"created": 1698957206,
"owned_by": "system"
},
{
"id": "gpt-3.5-turbo-1106",
"object": "model",
"created": 1698959748,
"owned_by": "system"
},
{
"id": "tts-1-hd",
"object": "model",
"created": 1699046015,
"owned_by": "system"
},
{
"id": "tts-1-1106",
"object": "model",
"created": 1699053241,
"owned_by": "system"
},
{
"id": "tts-1-hd-1106",
"object": "model",
"created": 1699053533,
"owned_by": "system"
},
{
"id": "text-embedding-3-small",
"object": "model",
"created": 1705948997,
"owned_by": "system"
},
{
"id": "text-embedding-3-large",
"object": "model",
"created": 1705953180,
"owned_by": "system"
},
{
"id": "gpt-4-0125-preview",
"object": "model",
"created": 1706037612,
"owned_by": "system"
},
{
"id": "gpt-4-turbo-preview",
"object": "model",
"created": 1706037777,
"owned_by": "system"
},
{
"id": "gpt-3.5-turbo-0125",
"object": "model",
"created": 1706048358,
"owned_by": "system"
},
{
"id": "gpt-4-turbo",
"object": "model",
"created": 1712361441,
"owned_by": "system"
},
{
"id": "gpt-4-turbo-2024-04-09",
"object": "model",
"created": 1712601677,
"owned_by": "system"
},
{
"id": "gpt-4o",
"object": "model",
"created": 1715367049,
"owned_by": "system"
},
{
"id": "gpt-4o-2024-05-13",
"object": "model",
"created": 1715368132,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-2024-07-18",
"object": "model",
"created": 1721172717,
"owned_by": "system"
},
{
"id": "gpt-4o-mini",
"object": "model",
"created": 1721172741,
"owned_by": "system"
},
{
"id": "gpt-4o-2024-08-06",
"object": "model",
"created": 1722814719,
"owned_by": "system"
},
{
"id": "chatgpt-4o-latest",
"object": "model",
"created": 1723515131,
"owned_by": "system"
},
{
"id": "o1-mini-2024-09-12",
"object": "model",
"created": 1725648979,
"owned_by": "system"
},
{
"id": "o1-mini",
"object": "model",
"created": 1725649008,
"owned_by": "system"
},
{
"id": "gpt-4o-realtime-preview-2024-10-01",
"object": "model",
"created": 1727131766,
"owned_by": "system"
},
{
"id": "gpt-4o-audio-preview-2024-10-01",
"object": "model",
"created": 1727389042,
"owned_by": "system"
},
{
"id": "gpt-4o-audio-preview",
"object": "model",
"created": 1727460443,
"owned_by": "system"
},
{
"id": "gpt-4o-realtime-preview",
"object": "model",
"created": 1727659998,
"owned_by": "system"
},
{
"id": "omni-moderation-latest",
"object": "model",
"created": 1731689265,
"owned_by": "system"
},
{
"id": "omni-moderation-2024-09-26",
"object": "model",
"created": 1732734466,
"owned_by": "system"
},
{
"id": "gpt-4o-realtime-preview-2024-12-17",
"object": "model",
"created": 1733945430,
"owned_by": "system"
},
{
"id": "gpt-4o-audio-preview-2024-12-17",
"object": "model",
"created": 1734034239,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-realtime-preview-2024-12-17",
"object": "model",
"created": 1734112601,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-audio-preview-2024-12-17",
"object": "model",
"created": 1734115920,
"owned_by": "system"
},
{
"id": "o1-2024-12-17",
"object": "model",
"created": 1734326976,
"owned_by": "system"
},
{
"id": "o1",
"object": "model",
"created": 1734375816,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-realtime-preview",
"object": "model",
"created": 1734387380,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-audio-preview",
"object": "model",
"created": 1734387424,
"owned_by": "system"
},
{
"id": "o3-mini",
"object": "model",
"created": 1737146383,
"owned_by": "system"
},
{
"id": "o3-mini-2025-01-31",
"object": "model",
"created": 1738010200,
"owned_by": "system"
},
{
"id": "gpt-4o-2024-11-20",
"object": "model",
"created": 1739331543,
"owned_by": "system"
},
{
"id": "gpt-4o-search-preview-2025-03-11",
"object": "model",
"created": 1741388170,
"owned_by": "system"
},
{
"id": "gpt-4o-search-preview",
"object": "model",
"created": 1741388720,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-search-preview-2025-03-11",
"object": "model",
"created": 1741390858,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-search-preview",
"object": "model",
"created": 1741391161,
"owned_by": "system"
},
{
"id": "gpt-4o-transcribe",
"object": "model",
"created": 1742068463,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-transcribe",
"object": "model",
"created": 1742068596,
"owned_by": "system"
},
{
"id": "o1-pro-2025-03-19",
"object": "model",
"created": 1742251504,
"owned_by": "system"
},
{
"id": "o1-pro",
"object": "model",
"created": 1742251791,
"owned_by": "system"
},
{
"id": "gpt-4o-mini-tts",
"object": "model",
"created": 1742403959,
"owned_by": "system"
},
{
"id": "o3-2025-04-16",
"object": "model",
"created": 1744133301,
"owned_by": "system"
},
{
"id": "o4-mini-2025-04-16",
"object": "model",
"created": 1744133506,
"owned_by": "system"
},
{
"id": "o3",
"object": "model",
"created": 1744225308,
"owned_by": "system"
},
{
"id": "o4-mini",
"object": "model",
"created": 1744225351,
"owned_by": "system"
},
{
"id": "gpt-4.1-2025-04-14",
"object": "model",
"created": 1744315746,
"owned_by": "system"
},
{
"id": "gpt-4.1",
"object": "model",
"created": 1744316542,
"owned_by": "system"
},
{
"id": "gpt-4.1-mini-2025-04-14",
"object": "model",
"created": 1744317547,
"owned_by": "system"
},
{
"id": "gpt-4.1-mini",
"object": "model",
"created": 1744318173,
"owned_by": "system"
},
{
"id": "gpt-4.1-nano-2025-04-14",
"object": "model",
"created": 1744321025,
"owned_by": "system"
},
{
"id": "gpt-4.1-nano",
"object": "model",
"created": 1744321707,
"owned_by": "system"
},
{
"id": "gpt-image-1",
"object": "model",
"created": 1745517030,
"owned_by": "system"
},
{
"id": "codex-mini-latest",
"object": "model",
"created": 1746673257,
"owned_by": "system"
},
{
"id": "o3-pro",
"object": "model",
"created": 1748475349,
"owned_by": "system"
},
{
"id": "gpt-4o-realtime-preview-2025-06-03",
"object": "model",
"created": 1748907838,
"owned_by": "system"
},
{
"id": "gpt-4o-audio-preview-2025-06-03",
"object": "model",
"created": 1748908498,
"owned_by": "system"
},
{
"id": "o3-pro-2025-06-10",
"object": "model",
"created": 1749166761,
"owned_by": "system"
},
{
"id": "o4-mini-deep-research",
"object": "model",
"created": 1749685485,
"owned_by": "system"
},
{
"id": "o3-deep-research",
"object": "model",
"created": 1749840121,
"owned_by": "system"
},
{
"id": "o3-deep-research-2025-06-26",
"object": "model",
"created": 1750865219,
"owned_by": "system"
},
{
"id": "o4-mini-deep-research-2025-06-26",
"object": "model",
"created": 1750866121,
"owned_by": "system"
},
{
"id": "gpt-5-chat-latest",
"object": "model",
"created": 1754073306,
"owned_by": "system"
},
{
"id": "gpt-5-2025-08-07",
"object": "model",
"created": 1754075360,
"owned_by": "system"
},
{
"id": "gpt-3.5-turbo-16k",
"object": "model",
"created": 1683758102,
"owned_by": "openai-internal"
},
{
"id": "tts-1",
"object": "model",
"created": 1681940951,
"owned_by": "openai-internal"
},
{
"id": "whisper-1",
"object": "model",
"created": 1677532384,
"owned_by": "openai-internal"
},
{
"id": "text-embedding-ada-002",
"object": "model",
"created": 1671217299,
"owned_by": "openai-internal"
}
]
}
@@ -0,0 +1,43 @@
Simple request + error which occurs before streaming begins (where applicable).
-- request --
{
"messages": [
{
"role": "user",
"content": "how many angels can dance on the head of a pin\n"
}
],
"model": "gpt-4.1",
"stream": true
}
-- streaming --
HTTP/2.0 400 Bad Request
Content-Length: 281
Content-Type: application/json
{
"error": {
"message": "Input tokens exceed the configured limit of 272000 tokens. Your messages resulted in 3148588 tokens. Please reduce the length of the messages.",
"type": "invalid_request_error",
"param": "messages",
"code": "context_length_exceeded"
}
}
-- non-streaming --
HTTP/2.0 400 Bad Request
Content-Length: 281
Content-Type: application/json
{
"error": {
"message": "Input tokens exceed the configured limit of 272000 tokens. Your messages resulted in 3148588 tokens. Please reduce the length of the messages.",
"type": "invalid_request_error",
"param": "messages",
"code": "context_length_exceeded"
}
}
@@ -0,0 +1,536 @@
Simple request.
-- request --
{
"messages": [
{
"role": "user",
"content": "how many angels can dance on the head of a pin\n"
}
],
"model": "gpt-4.1"
}
-- streaming --
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" \""},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"How"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" many"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" angels"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" dance"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" on"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" head"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" pin"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"?\""},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" classic"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" example"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ph"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ilos"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"oph"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ical"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theological"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" r"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"iddle"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**,"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" not"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" genuine"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" inquiry"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" metaph"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ysical"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" realities"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" The"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" phrase"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" most"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" likely"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" originated"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" during"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"med"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ieval"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" schol"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"astic"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" debates"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**,"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" where"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" scholars"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" engaged"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" complex"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" discussions"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" nature"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" spiritual"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" beings"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" limits"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" human"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" knowledge"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"###"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Meaning"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Context"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Not"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" meant"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" have"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" literal"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" answer"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Angels"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Christian"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theology"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" are"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" spiritual"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" ("},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"not"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" physical"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":")"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" beings"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" so"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" they"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" don"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"t"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" occupy"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" space"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" physical"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" sense"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Symbol"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ic"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" purpose"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" The"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" often"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" used"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" mock"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" illustrate"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" arguments"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" perceived"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" as"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" overly"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" speculative"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" irrelevant"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"###"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" \""},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Answers"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\""},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" through"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" History"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Sch"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ol"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ast"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ics"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" There's"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" little"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" evidence"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" medieval"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" scholars"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" literally"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" debated"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" this"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":";"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" it's"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" more"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" later"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"car"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ic"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"ature"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" their"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" intricate"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theological"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" arguments"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" **"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Modern"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" usage"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":":**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" It's"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" cited"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" as"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" an"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" example"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" pointless"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" un"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"answer"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"able"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"###"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" Summary"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"There"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" no"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" specific"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" number"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":";"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"**"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" rhetorical"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" highlighting"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" limits"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" theoretical"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" speculative"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" reasoning"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"Would"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" like"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" know"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" more"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" medieval"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" schol"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"astic"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" debates"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" or"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" how"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" this"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" used"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" modern"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" discourse"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[],"usage":{"prompt_tokens":19,"completion_tokens":238,"total_tokens":257,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}}
data: [DONE]
-- non-streaming --
{
"id": "chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N",
"object": "chat.completion",
"created": 1753357765,
"model": "gpt-4.1-2025-04-14",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The question \"How many angels can dance on the head of a pin?\" is a classic example of a rhetorical or philosophical question—*not* a real theological inquiry.\n\n**Origin and Meaning:**\n- The phrase is used to lampoon or satirize overly subtle, speculative, or irrelevant philosophical debates, especially those attributed to medieval scholasticism.\n- There is **no actual historical record** of medieval theologians debating this specific question.\n- It **illustrates debates about the nature of angels**—whether they occupy physical space, for example—but not in such literal terms.\n\n**If answered literally:**\n- If angels are considered non-corporeal and not limited by physical space, **an infinite number** could \"dance\" on the head of a pin.\n- If taken as a joke, the answer is up to the storyteller!\n\n**In summary:** \nIt's a facetious question highlighting the limits or absurdities of some philosophical or theological arguments. There is no fixed answer.",
"refusal": null,
"annotations": []
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 19,
"completion_tokens": 200,
"total_tokens": 219,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_b3f1157249"
}
@@ -0,0 +1,102 @@
LLM (https://llm.datasette.io/) configured with a simple "read_file" tool.
-- request --
{
"messages": [
{
"role": "user",
"content": "how large is the README.md file in my current path"
}
],
"model": "gpt-4.1",
"tools": [
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read the contents of a file at the given path.",
"parameters": {
"properties": {
"path": {
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
}
}
]
}
-- streaming --
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_HjeqP7YeRkoNj0de9e3U4X4B","type":"function","function":{"name":"read_file","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"path"}}]},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"README"}}]},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":".md"}}]},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null}
data: {"id":"chatcmpl-BwkwXxA0yAyLKZelloERJWtxKor9z","object":"chat.completion.chunk","created":1753343173,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_b3f1157249","choices":[],"usage":{"prompt_tokens":60,"completion_tokens":15,"total_tokens":75,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}}}
data: [DONE]
-- non-streaming --
{
"id": "chatcmpl-BwkyFElDIr1egmFyfQ9z4vPBto7m2",
"object": "chat.completion",
"created": 1753343279,
"model": "gpt-4.1-2025-04-14",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_KjzAbhiZC6nk81tQzL7pwlpc",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\":\"README.md\"}"
}
}
],
"refusal": null,
"annotations": []
},
"logprobs": null,
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 60,
"completion_tokens": 15,
"total_tokens": 75,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_b3f1157249"
}
@@ -0,0 +1,294 @@
Coder MCP tools automatically injected.
-- request --
{
"model": "gpt-4.1",
"messages": [
{
"role": "user",
"content": "list coder workspace IDs for admin"
}
]
}
-- streaming --
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ha7QSWuIrCLSg"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"I"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TxlRNztDyni152"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" am"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"d8rQaibDQpyL"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Qlbfp6UEp"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"68rb1Vo3ymBh"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" call"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"i7c6mc6zJY"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Z9syl1x73E7"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" appropriate"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5wK"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" tool"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"qxf0biXh4i"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UMXRLeWr9r7g"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" list"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"PkO0yHjNu3"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" all"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ktUBR7vT2FC"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" work"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xdNr1gCRJW"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"spaces"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5z5luvhUz"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"G6D7Ze3OlLR"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6BZ54FOiuA7"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" user"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6b0xOBQj2J"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" admin"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"X5gzNDQyO"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"oSONGErPa7g"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" display"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"EK9oGdN"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" their"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TPtBmjMIt"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" IDs"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"FONB73iSePd"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":".\n\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"VMpWnam5jp"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_0TxntkwDB66KH8z4RwNqeWrZ","type":"function","function":{"name":"bmcp_coder_coder_list_workspaces","arguments":""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kY"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"n5"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"owner"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"admin"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"1t"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null,"obfuscation":"sDj"}
data: {"id":"chatcmpl-C1WTooFaxeQgtyLB1kg53t41aB0NV","object":"chat.completion.chunk","created":1754479216,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[],"usage":{"prompt_tokens":4862,"completion_tokens":45,"total_tokens":4907,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"8sIWE1chOW"}
data: [DONE]
-- streaming/tool-call --
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"DBu9uyty0Uhux"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"Here"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Pk0tDwr0wkd"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" are"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ACu9WW1Lsz4"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xrXWRUKKAZl"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" workspace"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"LowCw"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" IDs"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"RXNpYewll1k"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"WnyxJrani1M"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" the"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"JrnDAJOLap4"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" user"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"RNZIdDo4vj"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" admin"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"nJ7O0qcsG"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":":\n\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"0k0UVPjnE2"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"dtGIleZ8Nl9lU7"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" Workspace"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wKNWu"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" Name"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"cmzvcWMEIp"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"GsImQO12UCnPHY"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" bob"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"AR4Jvn87StW"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"WoNeyT7BKKjIS"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2Ou4DytumVPlyW"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" Workspace"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"PRWw3"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" ID"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"rrKKjluNdVET"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"v6NUOTV1Pd6piU"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" dd"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UuYGjaLT7OXO"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"711"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"vLHjJVhbJgec"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"d"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2yDtuCir4L9eyS"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"5"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kyJOHcdfo1NMrP"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"nuKRieC0bpf6O3"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"q29JHHRnNg1GYt"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"83"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"e0o7Zu6eKnter"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"NCASF3SYR9GDQl"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"6"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"eG48V9XgxodtbB"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"CpP8ALTDfT0yBv"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"4"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"uQY85IhRAfuFl9"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wsdJSv3bN65S5a"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"08"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"dq2JARx8gsgIm"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-a"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"4booyOM91IZdC"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"0"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wVJJDjNFBXO3OC"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"af"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"XFtDbXdnHdnF3"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"-b"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"juymtEmZxo1Ez"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"730"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"8pIOLoJZJAfe"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"559"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"NPfQJmrtGPlY"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"06"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"jsqxOojcWTY3A"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"e"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"cWYFwWie0ciIju"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"8"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ilVWzWQLUWQOMw"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"c"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ea99MtCCypPar2"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"\n\n"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"SDq7UD3LcH7"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"Let"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"S343Ji05lUgD"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" me"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TTCD9vPg98sO"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" know"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xcsP3lRI6f"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" if"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"bS0qh0vq73n3"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"pxUYdxCHoy8"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" need"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wjLDXO4uD8"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" more"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"B6ckyharjv"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" information"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xrN"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" about"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"aqv4RrWxJ"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" any"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"hqdG5QSND4E"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" of"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"HvfgjMOXU6aG"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" these"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"yE0jSPMkD"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":" work"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wWfGxJR2wt"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"spaces"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"hOXndth8X"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"MReMwESHIpaDyo"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"EFeFvdS8m"}
data: {"id":"chatcmpl-C1WTqhYgK7bV01bW98Lww3zqaf8ZF","object":"chat.completion.chunk","created":1754479218,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_799e4ca3f1","choices":[],"usage":{"prompt_tokens":5049,"completion_tokens":60,"total_tokens":5109,"prompt_tokens_details":{"cached_tokens":4864,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"0JQt7Fw"}
data: [DONE]
-- non-streaming --
{
"id": "chatcmpl-C1XAKDTVYnmWS7tgvg7vPje00PIiy",
"object": "chat.completion",
"created": 1754481852,
"model": "gpt-4.1-2025-04-14",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "I am about to call the relevant function to list all workspaces for the user admin and provide their workspace IDs.\n\nExecuting the function call now.",
"tool_calls": [
{
"id": "call_aEuQAWKQYInC6fQ4z0iatdVP",
"type": "function",
"function": {
"name": "bmcp_coder_coder_list_workspaces",
"arguments": "{\"owner\":\"admin\"}"
}
}
],
"refusal": null,
"annotations": []
},
"logprobs": null,
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 4862,
"completion_tokens": 45,
"total_tokens": 4914,
"prompt_tokens_details": {
"cached_tokens": 0,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_51e1070cf2"
}
-- non-streaming/tool-call --
{
"id": "chatcmpl-C1XANLwdflVxAjKOjbMP3LJxSlXsS",
"object": "chat.completion",
"created": 1754481855,
"model": "gpt-4.1-2025-04-14",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Here is the list of Coder workspace IDs for the user admin:\n\n- Workspace Name: bob\n- Workspace ID: dd711d5c-83c6-4c08-a0af-b73055906e8c\n\nLet me know if you need more details or actions on this workspace!",
"refusal": null,
"annotations": []
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 5049,
"completion_tokens": 60,
"total_tokens": 5119,
"prompt_tokens_details": {
"cached_tokens": 4864,
"audio_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"audio_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"service_tier": "default",
"system_fingerprint": "fp_51e1070cf2"
}
@@ -0,0 +1,25 @@
Simple request + error.
-- request --
{
"messages": [
{
"role": "user",
"content": "how many angels can dance on the head of a pin\n"
}
],
"model": "gpt-4.1",
"stream": true
}
-- streaming --
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":"The"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" question"},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"id":"chatcmpl-BwoiPTGRbKkY5rncfaM0s9KtWrq5N","object":"chat.completion.chunk","created":1753357673,"model":"gpt-4.1-2025-04-14","service_tier":"default","system_fingerprint":"fp_51e1070cf2","choices":[{"index":0,"delta":{"content":" \""},"logprobs":null,"finish_reason":null}],"usage":null}
data: {"error": {"message": "The server had an error while processing your request. Sorry about that!", "type": "server_error"}}
@@ -0,0 +1,73 @@
Streaming response where the provider returns an injected tool call as the first chunk with no text preamble.
This test ensures tool invocation continues even when no chunks are relayed to the client.
-- request --
{
"messages": [
{
"content": "<current_datetime>2026-01-22T18:35:17.612Z</current_datetime>\n\nlist all my coder workspaces",
"role": "user"
}
],
"model": "claude-haiku-4.5",
"n": 1,
"temperature": 1,
"parallel_tool_calls": false,
"stream_options": {
"include_usage": true
},
"stream": true
}
-- streaming --
data: {"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"function":{"name":"bmcp_coder_coder_list_workspaces"},"id":"toolu_vrtx_01CvBi1d4qpKTG2PCuc9wDbZ","index":0,"type":"function"}]}}],"created":1769106921,"id":"msg_vrtx_01UoiRJwj3JXcwNYAh3z7ARs","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"function":{"arguments":""},"index":0}]}}],"created":1769106921,"id":"msg_vrtx_01UoiRJwj3JXcwNYAh3z7ARs","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"function":{"arguments":"{\"own"},"index":0}]}}],"created":1769106921,"id":"msg_vrtx_01UoiRJwj3JXcwNYAh3z7ARs","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"function":{"arguments":"er\": \"me\"}"},"index":0}]}}],"created":1769106921,"id":"msg_vrtx_01UoiRJwj3JXcwNYAh3z7ARs","model":"claude-haiku-4.5"}
data: {"choices":[{"finish_reason":"tool_calls","index":0,"delta":{"content":null}}],"created":1769106921,"id":"msg_vrtx_01UoiRJwj3JXcwNYAh3z7ARs","usage":{"completion_tokens":65,"prompt_tokens":25716,"prompt_tokens_details":{"cached_tokens":20470},"total_tokens":25781},"model":"claude-haiku-4.5"}
data: [DONE]
-- streaming/tool-call --
data: {"choices":[{"index":0,"delta":{"content":"You","role":"assistant"}}],"created":1769198061,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" have one","role":"assistant"}}],"created":1769198061,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" Coder workspace:","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"\n\n**test-scf** (","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"ID: a174a2e5","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"-5050-445d-89","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"ff-dd720e5b442","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"e)\n- Template: docker","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"\n- Template Version","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" ID","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":": ad1b5ab1-","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"fc18-4792-84f","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"7-797787607d30","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"\n- Status","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":": Up","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" to date","role":"assistant"}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","model":"claude-haiku-4.5"}
data: {"choices":[{"finish_reason":"stop","index":0,"delta":{"content":null}}],"created":1769198062,"id":"msg_vrtx_015B1npskreQgEjMrfsdjH1m","usage":{"completion_tokens":85,"prompt_tokens":25989,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":26074},"model":"claude-haiku-4.5"}
data: [DONE]
@@ -0,0 +1,72 @@
Streaming response where the provider returns text content followed by an injected tool call at index 1 (instead of index 0).
This can happen when the provider incorrectly continues indexing from a previous response.
This tests that nil entries are removed from the tool calls array caused by non-zero starting indices.
-- request --
{
"messages": [
{
"content": "<current_datetime>2026-01-23T20:22:43.781Z</current_datetime>\n\nI want you to do to this in order:\n1) create a file in my current directory with name \"test.txt\"\n2) list all my coder workspaces",
"role": "user"
}
],
"model": "claude-haiku-4.5",
"n": 1,
"temperature": 1,
"parallel_tool_calls": false,
"stream_options": {
"include_usage": true
},
"stream": true
}
-- streaming --
data: {"choices":[{"index":0,"delta":{"content":"Now","role":"assistant"}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" listing","role":"assistant"}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" your","role":"assistant"}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" C","role":"assistant"}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"oder workspaces:","role":"assistant"}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"function":{"name":"bmcp_coder_coder_list_workspaces"},"id":"toolu_vrtx_01DbFqUgk6aAtJ4nDBqzFWDF","index":1,"type":"function"}]}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":null,"tool_calls":[{"function":{"arguments":""},"index":1}]}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","model":"claude-haiku-4.5"}
data: {"choices":[{"finish_reason":"tool_calls","index":0,"delta":{"content":null}}],"created":1769199774,"id":"msg_vrtx_01Fiieb5Z3kqJf9a3FwvLkky","usage":{"completion_tokens":58,"prompt_tokens":25939,"prompt_tokens_details":{"cached_tokens":25429},"total_tokens":25997},"model":"claude-haiku-4.5"}
data: [DONE]
-- streaming/tool-call --
data: {"choices":[{"index":0,"delta":{"content":"Done","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"! I create","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"d `","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"test.txt` in","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" your current directory.","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" You","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" have","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" 1","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" ","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":"Coder workspace:\n\n-","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" **test-scf** (docker","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"index":0,"delta":{"content":" template)","role":"assistant"}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","model":"claude-haiku-4.5"}
data: {"choices":[{"finish_reason":"stop","index":0,"delta":{"content":null}}],"created":1769199776,"id":"msg_vrtx_01RVxamMyw1DBtpoENDpmnQK","usage":{"completion_tokens":39,"prompt_tokens":26166,"prompt_tokens_details":{"cached_tokens":25934},"total_tokens":26205},"model":"claude-haiku-4.5"}
data: [DONE]
@@ -0,0 +1,81 @@
-- request --
{
"input": "This was a large input...",
"model": "gpt-4.1",
"prompt_cache_key": "key-123",
"prompt_cache_retention": "24h",
"stream": false
}
-- non-streaming --
{
"id": "resp_0cd5d6b8310055d600696a1776b42c81a199fbb02248a8bfa0",
"object": "response",
"created_at": 1768560502,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1768560504,
"error": null,
"frequency_penalty": 0.0,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4.1-2025-04-14",
"output": [
{
"id": "msg_0cd5d6b8310055d600696a177708b881a1bb53034def764104",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "- I provide clear, accurate, and concise answers tailored to your requests.\n- I can process and summarize large volumes of information quickly.\n- I adapt my responses based on your needs and instructions for precision and relevance."
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"presence_penalty": 0.0,
"previous_response_id": null,
"prompt_cache_key": "key-123",
"prompt_cache_retention": "24h",
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 12033,
"input_tokens_details": {
"cached_tokens": 11904
},
"output_tokens": 44,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 12077
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,139 @@
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-5.4",
"stream": false,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- non-streaming --
{
"id": "resp_0aba2ac43dc240b30169b15720243c819ebb64977365d42cf5",
"object": "response",
"created_at": 1773229856,
"status": "completed",
"background": false,
"completed_at": 1773229861,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-5.4-2026-03-05",
"output": [
{
"id": "rs_0aba2ac43dc240b30169b157208c88819e8238a91b5f7a919b",
"type": "reasoning",
"status": "completed",
"encrypted_content": "gAAAAA==",
"summary": []
},
{
"id": "msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"text": "Checking whether 3 + 5 is prime by calling the add function first."
}
],
"phase": "commentary",
"role": "assistant"
},
{
"id": "fc_0aba2ac43dc240b30169b157255604819e8a108124efc1635c",
"type": "function_call",
"status": "completed",
"arguments": "{\"a\":3,\"b\":5}",
"call_id": "call_A8TkZmIcKtw2Zw952Wc5QVe7",
"name": "add"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": "xhigh",
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": false,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "low"
},
"tool_choice": "auto",
"tools": [
{
"type": "function",
"description": "Add two numbers together.",
"name": "add",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
],
"additionalProperties": false
},
"strict": true
}
],
"top_logprobs": 0,
"top_p": 0.98,
"truncation": "disabled",
"usage": {
"input_tokens": 58,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 30,
"output_tokens_details": {
"reasoning_tokens": 10
},
"total_tokens": 88
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,82 @@
-- request --
{
"conversation": "conv_695fa15ecbb881958e89ac2d35d918ed0c9f1f0524a858fa",
"input": "explain why this is funny.",
"model": "gpt-4o-mini",
"stream": false
}
-- non-streaming --
{
"id": "resp_0c9f1f0524a858fa00695fa15fc5a081958f4304aafd3bdec2",
"object": "response",
"created_at": 1767874911,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1767874914,
"conversation": {
"id": "conv_695fa15ecbb881958e89ac2d35d918ed0c9f1f0524a858fa"
},
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_0c9f1f0524a858fa00695fa1605bd48195b65b4dfd732941bc",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "This joke plays on a double meaning of the phrase \u201cmake up.\u201d \n\n1. **Literal Meaning**: Atoms are the basic building blocks of matter and literally \"make up\" all substances in the universe.\n\n2. **Figurative Meaning**: The phrase \"make up\" can also mean to fabricate or lie about something. \n\nThe humor comes from the unexpected twist; it starts off sounding like a serious statement about atoms, then surprises us with a clever play on words that suggests atoms are dishonest. This blend of scientific fact and pun creates the comedic effect!"
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 48,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 116,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 164
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,93 @@
-- request --
{
"input": "Use the code_exec tool to print hello world to the console.",
"model": "gpt-5",
"tools": [
{
"type": "custom",
"name": "code_exec",
"description": "Executes arbitrary Python code."
}
]
}
-- non-streaming --
{
"id": "resp_09c614364030cdf000696942589da081a0af07f5859acb7308",
"object": "response",
"created_at": 1768505944,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1768505948,
"error": null,
"frequency_penalty": 0.0,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-5-2025-08-07",
"output": [
{
"id": "rs_09c614364030cdf00069694258e45881a0b8d5f198cde47d58",
"type": "reasoning",
"summary": []
},
{
"id": "ctc_09c614364030cdf0006969425bf33481a09cc0f9522af2d980",
"type": "custom_tool_call",
"status": "completed",
"call_id": "call_haf8njtwrVZ1754Gm6fjAtuA",
"input": "print(\"hello world\")",
"name": "code_exec"
}
],
"parallel_tool_calls": true,
"presence_penalty": 0.0,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": "medium",
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [
{
"type": "custom",
"description": "Executes arbitrary Python code.",
"format": {
"type": "text"
},
"name": "code_exec"
}
],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 64,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 148,
"output_tokens_details": {
"reasoning_tokens": 128
},
"total_tokens": 212
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,21 @@
-- request --
{
"input": "tell me a joke",
"model": "gpt-4o-mini",
"stream": false
}
-- non-streaming --
HTTP/2.0 401 Unauthorized
Content-Length: 234
Content-Type: application/json
{
"error": {
"message": "Incorrect API key provided: sk-***. You can find your API key at https://platform.openai.com/account/api-keys.",
"type": "authentication_error",
"param": null,
"code": "invalid_api_key"
}
}
@@ -0,0 +1,142 @@
Two reasoning output items before a function_call.
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-4.1",
"stream": false,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- non-streaming --
{
"id": "resp_0da6045a8b68fa5200695fa23dcc2c81a19c849f627abf8a31",
"object": "response",
"created_at": 1767875133,
"status": "completed",
"background": false,
"completed_at": 1767875134,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4.1-2025-04-14",
"output": [
{
"id": "rs_0da6045a8b68fa5200695fa23e100081a19bf68887d47ae93d",
"type": "reasoning",
"status": "completed",
"summary": [
{
"type": "summary_text",
"text": "The user wants to add 3 and 5. Let me call the add function."
}
]
},
{
"id": "rs_1aa7045a8b68fa5200695fa23e200082b29cf79998e58bf94e",
"type": "reasoning",
"status": "completed",
"summary": [
{
"type": "summary_text",
"text": "After adding, I will check if the result is prime."
}
]
},
{
"id": "fc_0da6045a8b68fa5200695fa23e198081a19bf68887d47ae93d",
"type": "function_call",
"status": "completed",
"arguments": "{\"a\":3,\"b\":5}",
"call_id": "call_CJSaa2u51JG996575oVljuNq",
"name": "add"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [
{
"type": "function",
"description": "Add two numbers together.",
"name": "add",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
],
"additionalProperties": false
},
"strict": true
}
],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 58,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 18,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 76
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,78 @@
-- request --
{
"input": "explain why this is funny.",
"model": "gpt-4o-mini",
"previous_response_id": "resp_0388c79043df3e3400695f9f83cd6481959062cec6830d8d51",
"stream": false
}
-- non-streaming --
{
"id": "resp_0388c79043df3e3400695f9f86cfa08195af1f015c60117a83",
"object": "response",
"created_at": 1767874438,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1767874441,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_0388c79043df3e3400695f9f87369c8195a0d1a82a06f96d56",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "The joke plays on a clever wordplay and a double meaning. \n\n1. **Outstanding in his field**: The phrase can mean that someone is exceptionally good at what they do (outstanding performance) and also literally refers to the scarecrow being in a field (like a farm field). \n\n2. **Scarecrow context**: Scarecrows are placed in fields to scare away birds, so the idea of a scarecrow being \"outstanding\" can lead to a funny mental image.\n\nThe humor comes from the unexpected twist of a literal phrase being interpreted in a figurative way, creating a light and playful pun."
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": "resp_0388c79043df3e3400695f9f83cd6481959062cec6830d8d51",
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 43,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 129,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 172
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,77 @@
-- request --
{
"input": "tell me a joke",
"model": "gpt-4o-mini",
"stream": false
}
-- non-streaming --
{
"id": "resp_0388c79043df3e3400695f9f83cd6481959062cec6830d8d51",
"object": "response",
"created_at": 1767874435,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1767874436,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_0388c79043df3e3400695f9f8447a08195af2ef951966823c4",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "Why did the scarecrow win an award?\n\nBecause he was outstanding in his field!"
}
],
"role": "assistant"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 11,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 18,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 29
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,132 @@
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-4.1",
"stream": false,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- non-streaming --
{
"id": "resp_0da6045a8b68fa5200695fa23dcc2c81a19c849f627abf8a31",
"object": "response",
"created_at": 1767875133,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1767875134,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4.1-2025-04-14",
"output": [
{
"id": "rs_0da6045a8b68fa5200695fa23e100081a19bf68887d47ae93d",
"type": "reasoning",
"status": "completed",
"summary": [
{
"type": "summary_text",
"text": "The user wants to add 3 and 5. Let me call the add function."
}
]
},
{
"id": "fc_0da6045a8b68fa5200695fa23e198081a19bf68887d47ae93d",
"type": "function_call",
"status": "completed",
"arguments": "{\"a\":3,\"b\":5}",
"call_id": "call_CJSaa2u51JG996575oVljuNq",
"name": "add"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [
{
"type": "function",
"description": "Add two numbers together.",
"name": "add",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
],
"additionalProperties": false
},
"strict": true
}
],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 58,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 18,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 76
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,140 @@
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Also add 10 + 20. Use the add function for both."
}
],
"model": "gpt-4.1",
"stream": false,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- non-streaming --
{
"id": "resp_parallel_blocking_001",
"object": "response",
"created_at": 1767875133,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1767875134,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4.1-2025-04-14",
"output": [
{
"id": "rs_parallel_blocking_reasoning_001",
"type": "reasoning",
"status": "completed",
"summary": [
{
"type": "summary_text",
"text": "The user wants two additions: 3+5 and 10+20. I'll call add for both."
}
]
},
{
"id": "fc_parallel_blocking_first_001",
"type": "function_call",
"status": "completed",
"arguments": "{\"a\":3,\"b\":5}",
"call_id": "call_ParallelBlockingFirst001",
"name": "add"
},
{
"id": "fc_parallel_blocking_second_001",
"type": "function_call",
"status": "completed",
"arguments": "{\"a\":10,\"b\":20}",
"call_id": "call_ParallelBlockingSecond01",
"name": "add"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": null,
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": true,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "medium"
},
"tool_choice": "auto",
"tools": [
{
"type": "function",
"description": "Add two numbers together.",
"name": "add",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
],
"additionalProperties": false
},
"strict": true
}
],
"top_logprobs": 0,
"top_p": 1.0,
"truncation": "disabled",
"usage": {
"input_tokens": 65,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 30,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 95
},
"user": null,
"metadata": {}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,146 @@
Both a reasoning summary and a commentary message before a function_call.
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-5.4",
"stream": false,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- non-streaming --
{
"id": "resp_1bba3bc54ed351c41270c26831354d920fcc75088476e53de6",
"object": "response",
"created_at": 1773229900,
"status": "completed",
"background": false,
"completed_at": 1773229905,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-5.4-2026-03-05",
"output": [
{
"id": "rs_1bba3bc54ed351c41270c26831908d920fcc75088476e53de6",
"type": "reasoning",
"status": "completed",
"encrypted_content": "gAAAAA==",
"summary": [
{
"type": "summary_text",
"text": "I need to add 3 and 5 to check primality."
}
]
},
{
"id": "msg_1bba3bc54ed351c41270c26831a09d920fdd86199587f64ef7",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"text": "Let me calculate the sum first using the add function."
}
],
"phase": "commentary",
"role": "assistant"
},
{
"id": "fc_1bba3bc54ed351c41270c26831b0ad920fee97200698074f08",
"type": "function_call",
"status": "completed",
"arguments": "{\"a\":3,\"b\":5}",
"call_id": "call_B9UjYX01Lvvv1XwjDsdmRW3f",
"name": "add"
}
],
"parallel_tool_calls": true,
"previous_response_id": null,
"prompt_cache_key": null,
"prompt_cache_retention": null,
"reasoning": {
"effort": "xhigh",
"summary": null
},
"safety_identifier": null,
"service_tier": "default",
"store": false,
"temperature": 1.0,
"text": {
"format": {
"type": "text"
},
"verbosity": "low"
},
"tool_choice": "auto",
"tools": [
{
"type": "function",
"description": "Add two numbers together.",
"name": "add",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
],
"additionalProperties": false
},
"strict": true
}
],
"top_logprobs": 0,
"top_p": 0.98,
"truncation": "disabled",
"usage": {
"input_tokens": 58,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 35,
"output_tokens_details": {
"reasoning_tokens": 10
},
"total_tokens": 93
},
"user": null,
"metadata": {}
}
@@ -0,0 +1,39 @@
-- request --
{
"input": "hello",
"model": "gpt-6.7"
}
-- non-streaming --
{
"id": "resp_0388c79043df3e3400695f9f83cd6481959062cec6830d8d51",
"object": "response",
"created_at": 1767874435,
"status": "completed",
"background": false,
"billing": {
"payer": "developer"
},
"completed_at": 1767874436,
"error": null,
"incomplete_details": null,
"instructions": null,
"max_output_tokens": null,
"max_tool_calls": null,
"model": "gpt-4o-mini-2024-07-18",
"output": [
{
"id": "msg_0388c79043df3e3400695f9f8447a08195af2ef951966823c4",
"type": "message",
"status": "completed",
"content": [
{
"type": "output_text",
"annotations": [],
"logprobs": [],
"text": "This json is formatted wrong"
}
],
"role": "assistant"
}
],
@@ -0,0 +1,98 @@
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-4.1",
"stream": true,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_0c3fb28cfcf463a500695fa2f0239481a095ec6ce3dfe4d458","object":"response","created_at":1767875312,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_0c3fb28cfcf463a500695fa2f0239481a095ec6ce3dfe4d458","object":"response","created_at":1767875312,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","type":"reasoning","status":"in_progress","summary":[]},"output_index":0,"sequence_number":2}
event: response.reasoning_summary_part.added
data: {"type":"response.reasoning_summary_part.added","item_id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","output_index":0,"part":{"type":"summary_text","text":""},"summary_index":0,"sequence_number":3}
event: response.reasoning_summary_text.delta
data: {"type":"response.reasoning_summary_text.delta","item_id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","output_index":0,"summary_index":0,"delta":"The user wants to add 3 and 5. Let me call the add function.","sequence_number":4}
event: response.reasoning_summary_text.done
data: {"type":"response.reasoning_summary_text.done","item_id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","output_index":0,"summary_index":0,"text":"The user wants to add 3 and 5. Let me call the add function.","sequence_number":5}
event: response.reasoning_summary_part.done
data: {"type":"response.reasoning_summary_part.done","item_id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","output_index":0,"part":{"type":"summary_text","text":"The user wants to add 3 and 5. Let me call the add function."},"summary_index":0,"sequence_number":6}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","type":"reasoning","status":"completed","summary":[{"type":"summary_text","text":"The user wants to add 3 and 5. Let me call the add function."}]},"output_index":0,"sequence_number":7}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","type":"function_call","status":"in_progress","arguments":"","call_id":"call_7VaiUXZYuuuwWwviCrckxq6t","name":"add"},"output_index":1,"sequence_number":8}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"{\"","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"gWZHP8i4lSgQYT","output_index":1,"sequence_number":9}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"a","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"yC1iubuqc098ZSH","output_index":1,"sequence_number":10}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"\":","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"G17nNbWUcJkqA2","output_index":1,"sequence_number":11}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"3","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"Mj71L4eeLZbIEFU","output_index":1,"sequence_number":12}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":",\"","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"ZchcCauvlPtVc7","output_index":1,"sequence_number":13}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"b","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"gWLYMrsBI3ZHKVP","output_index":1,"sequence_number":14}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"\":","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"n4iUzpnbPE4DnO","output_index":1,"sequence_number":15}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"5","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"23mO3rxkXqDOi6g","output_index":1,"sequence_number":16}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"}","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"AQnBsNz7GqkdylH","output_index":1,"sequence_number":17}
event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","arguments":"{\"a\":3,\"b\":5}","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","output_index":1,"sequence_number":18}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_7VaiUXZYuuuwWwviCrckxq6t","name":"add"},"output_index":1,"sequence_number":19}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_0c3fb28cfcf463a500695fa2f0239481a095ec6ce3dfe4d458","object":"response","created_at":1767875312,"status":"completed","background":false,"completed_at":1767875312,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[{"id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","type":"reasoning","status":"completed","summary":[{"type":"summary_text","text":"The user wants to add 3 and 5. Let me call the add function."}]},{"id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_7VaiUXZYuuuwWwviCrckxq6t","name":"add"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":58,"input_tokens_details":{"cached_tokens":0},"output_tokens":18,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":76},"user":null,"metadata":{}},"sequence_number":20}
@@ -0,0 +1,47 @@
-- request --
{
"model": "gpt-5.2-codex",
"input": "Test cached input tokens.",
"stream": true
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_05080461b406f3f501696a1409d34c8195a40ff4b092145c35","object":"response","created_at":1768559625,"status":"in_progress","background":false,"completed_at":null,"error":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.2-codex","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":"detailed"},"service_tier":"auto","store":false,"temperature":1.0,"tool_choice":"auto","tools":[],"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_05080461b406f3f501696a1409d34c8195a40ff4b092145c35","object":"response","created_at":1768559625,"status":"in_progress","background":false,"completed_at":null,"error":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.2-codex","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":"detailed"},"service_tier":"auto","store":false,"temperature":1.0,"tool_choice":"auto","tools":[],"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2}
event: response.content_part.added
data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"part":{"type":"output_text","annotations":[],"text":""},"sequence_number":3}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Test","item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"sequence_number":4}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" response","item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"sequence_number":5}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" with","item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"sequence_number":6}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" cached","item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"sequence_number":7}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" tokens.","item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"sequence_number":8}
event: response.output_text.done
data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"text":"Test response with cached tokens.","sequence_number":9}
event: response.content_part.done
data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","output_index":0,"part":{"type":"output_text","annotations":[],"text":"Test response with cached tokens."},"sequence_number":10}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"text":"Test response with cached tokens."}],"role":"assistant"},"output_index":0,"sequence_number":11}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_05080461b406f3f501696a1409d34c8195a40ff4b092145c35","object":"response","created_at":1768559625,"status":"completed","background":false,"completed_at":1768559627,"error":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.2-codex","output":[{"id":"msg_05080461b406f3f501696a140a70d88195a2ce4c1a4eb39696","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"text":"Test response with cached tokens."}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":"019bc657-f77b-7292-b5f4-2e8d6c2b0945","prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":"detailed"},"service_tier":"default","store":false,"temperature":1.0,"tool_choice":"auto","tools":[],"truncation":"disabled","usage":{"input_tokens":16909,"input_tokens_details":{"cached_tokens":15744},"output_tokens":54,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":16963},"user":null,"metadata":{}},"sequence_number":12}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,80 @@
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-5.4",
"stream": true,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_0aba2ac43dc240b30169b15720243c819ebb64977365d42cf5","object":"response","created_at":1773229856,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.4-2026-03-05","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"xhigh","summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"low"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_0aba2ac43dc240b30169b15720243c819ebb64977365d42cf5","object":"response","created_at":1773229856,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.4-2026-03-05","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"xhigh","summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"low"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"rs_0aba2ac43dc240b30169b157208c88819e8238a91b5f7a919b","type":"reasoning","status":"in_progress","summary":[]},"output_index":0,"sequence_number":2}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"rs_0aba2ac43dc240b30169b157208c88819e8238a91b5f7a919b","type":"reasoning","status":"completed","encrypted_content":"gAAAAA==","summary":[]},"output_index":0,"sequence_number":3}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","type":"message","status":"in_progress","content":[],"phase":"commentary","role":"assistant"},"output_index":1,"sequence_number":4}
event: response.content_part.added
data: {"type":"response.content_part.added","item_id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","output_index":1,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]},"sequence_number":5}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","output_index":1,"content_index":0,"delta":"Checking whether 3 + 5 is prime by calling the add function first.","sequence_number":6}
event: response.output_text.done
data: {"type":"response.output_text.done","item_id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","output_index":1,"content_index":0,"text":"Checking whether 3 + 5 is prime by calling the add function first.","sequence_number":7}
event: response.content_part.done
data: {"type":"response.content_part.done","item_id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","output_index":1,"content_index":0,"part":{"type":"output_text","text":"Checking whether 3 + 5 is prime by calling the add function first.","annotations":[]},"sequence_number":8}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"text":"Checking whether 3 + 5 is prime by calling the add function first."}],"phase":"commentary","role":"assistant"},"output_index":1,"sequence_number":9}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"fc_0aba2ac43dc240b30169b157255604819e8a108124efc1635c","type":"function_call","status":"in_progress","arguments":"","call_id":"call_A8TkZmIcKtw2Zw952Wc5QVe7","name":"add"},"output_index":2,"sequence_number":10}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"{\"a\":3,\"b\":5}","item_id":"fc_0aba2ac43dc240b30169b157255604819e8a108124efc1635c","output_index":2,"sequence_number":11}
event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","arguments":"{\"a\":3,\"b\":5}","item_id":"fc_0aba2ac43dc240b30169b157255604819e8a108124efc1635c","output_index":2,"sequence_number":12}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"fc_0aba2ac43dc240b30169b157255604819e8a108124efc1635c","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_A8TkZmIcKtw2Zw952Wc5QVe7","name":"add"},"output_index":2,"sequence_number":13}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_0aba2ac43dc240b30169b15720243c819ebb64977365d42cf5","object":"response","created_at":1773229856,"status":"completed","background":false,"completed_at":1773229861,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.4-2026-03-05","output":[{"id":"rs_0aba2ac43dc240b30169b157208c88819e8238a91b5f7a919b","type":"reasoning","status":"completed","encrypted_content":"gAAAAA==","summary":[]},{"id":"msg_0aba2ac43dc240b30169b1572286d0819eb24b1d0f84c8fb3f","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"text":"Checking whether 3 + 5 is prime by calling the add function first."}],"phase":"commentary","role":"assistant"},{"id":"fc_0aba2ac43dc240b30169b157255604819e8a108124efc1635c","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_A8TkZmIcKtw2Zw952Wc5QVe7","name":"add"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"xhigh","summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"low"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":{"input_tokens":58,"input_tokens_details":{"cached_tokens":0},"output_tokens":30,"output_tokens_details":{"reasoning_tokens":10},"total_tokens":88},"user":null,"metadata":{}},"sequence_number":14}
@@ -0,0 +1,540 @@
-- request --
{
"conversation": "conv_695fa1132770819795d013275c77e8380108ce40c6fb22bd",
"input": "explain why this is funny.",
"model": "gpt-4o-mini",
"stream": true
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_0108ce40c6fb22bd00695fa11395588197a8207c74e6e3795c","object":"response","created_at":1767874835,"status":"in_progress","background":false,"completed_at":null,"conversation":{"id":"conv_695fa1132770819795d013275c77e8380108ce40c6fb22bd"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_0108ce40c6fb22bd00695fa11395588197a8207c74e6e3795c","object":"response","created_at":1767874835,"status":"in_progress","background":false,"completed_at":null,"conversation":{"id":"conv_695fa1132770819795d013275c77e8380108ce40c6fb22bd"},"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2}
event: response.content_part.added
data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"This","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"6JuS91EMbhLA","output_index":0,"sequence_number":4}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" joke","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"y4aKJq6ioqK","output_index":0,"sequence_number":5}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"OSK1qGQlQ45Gf","output_index":0,"sequence_number":6}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" funny","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"xOx3biYzfi","output_index":0,"sequence_number":7}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" for","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"B6nzgMtFCPfI","output_index":0,"sequence_number":8}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"NLJ3uuUUR7HEwL","output_index":0,"sequence_number":9}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" couple","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"axMyCq7cc","output_index":0,"sequence_number":10}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"wogQAHGbERhyj","output_index":0,"sequence_number":11}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" reasons","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"kaIWALH5","output_index":0,"sequence_number":12}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":":\n\n","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"5aWCXnTSm1Ww0","output_index":0,"sequence_number":13}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"1","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ulbeCHj60aqERM2","output_index":0,"sequence_number":14}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"LS6N4ccoGtkBMf9","output_index":0,"sequence_number":15}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" **","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"RyhciW9kcGtT3","output_index":0,"sequence_number":16}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Word","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"JJOH0y2lt5ce","output_index":0,"sequence_number":17}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"play","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"FweyacD1kgKU","output_index":0,"sequence_number":18}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"**","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"99utx5f2PR410S","output_index":0,"sequence_number":19}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":":","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"dZe5PeQsygjpDJU","output_index":0,"sequence_number":20}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" The","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"3UfyKaxhlu5T","output_index":0,"sequence_number":21}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" humor","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"aTNqJJdtlA","output_index":0,"sequence_number":22}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" comes","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"xK3buVbUHt","output_index":0,"sequence_number":23}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" from","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"igWwXO0tQtm","output_index":0,"sequence_number":24}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"A39bwmGkGF3T","output_index":0,"sequence_number":25}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" double","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"nLeuH3WdF","output_index":0,"sequence_number":26}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" meaning","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zxC0qSSE","output_index":0,"sequence_number":27}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"DIMKV7wc7lnEa","output_index":0,"sequence_number":28}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"CnM6idZlt3Su","output_index":0,"sequence_number":29}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" phrase","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"DSxcKiYE2","output_index":0,"sequence_number":30}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" \"","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zKE75xC70J5I8n","output_index":0,"sequence_number":31}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"make","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"oBFujacYh6Qi","output_index":0,"sequence_number":32}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" up","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"MCWKA9PGFz3uH","output_index":0,"sequence_number":33}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".\"","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Mww11OYYfx46Pn","output_index":0,"sequence_number":34}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" In","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"lDHppT2E9fBjL","output_index":0,"sequence_number":35}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" one","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"qH7241nKwTjN","output_index":0,"sequence_number":36}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" sense","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"aQcSSHwJ3p","output_index":0,"sequence_number":37}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ZNoviZFdXYechTT","output_index":0,"sequence_number":38}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" atoms","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"nXkzWnQfut","output_index":0,"sequence_number":39}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" are","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"9IE6b6ePg9E6","output_index":0,"sequence_number":40}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"MN8puLH01K4r","output_index":0,"sequence_number":41}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" basic","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"cHHGWtl6sA","output_index":0,"sequence_number":42}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" building","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Qh8Lgl6","output_index":0,"sequence_number":43}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" blocks","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"usrQ4Zqhy","output_index":0,"sequence_number":44}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"UlMkWTr0buDdu","output_index":0,"sequence_number":45}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" matter","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"di7aKyqOB","output_index":0,"sequence_number":46}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" and","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Jz1ouMsSH5Sq","output_index":0,"sequence_number":47}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" literally","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"bcPU64","output_index":0,"sequence_number":48}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" \"","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"k0mzekJTeeeyjl","output_index":0,"sequence_number":49}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"make","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"osOddu5z1SKn","output_index":0,"sequence_number":50}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" up","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"hxVor1fqBr85z","output_index":0,"sequence_number":51}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"\"","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"R6QtJIz32R1BVio","output_index":0,"sequence_number":52}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" everything","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"AwhOH","output_index":0,"sequence_number":53}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" in","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"OumZOuQTLGWst","output_index":0,"sequence_number":54}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"aJI4Tm9Si3rt","output_index":0,"sequence_number":55}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" physical","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"F1cKqO8","output_index":0,"sequence_number":56}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" world","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"QNMNuZEBTi","output_index":0,"sequence_number":57}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"MXn5ZYICLy6vCbY","output_index":0,"sequence_number":58}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" In","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"NeupGqbEKerw6","output_index":0,"sequence_number":59}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" another","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"K8tdy7U8","output_index":0,"sequence_number":60}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" sense","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"pjhD3Np58X","output_index":0,"sequence_number":61}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ACou7OILpf3wWDR","output_index":0,"sequence_number":62}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" \"","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"L4nsA8ZF0swWRP","output_index":0,"sequence_number":63}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"making","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"loHLh0D52x","output_index":0,"sequence_number":64}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" up","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ZCbUNkX3fmHK5","output_index":0,"sequence_number":65}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"\"","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"B9vFmLYXf6C0spM","output_index":0,"sequence_number":66}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" something","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"qYs53A","output_index":0,"sequence_number":67}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" can","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zZfzpKfcLO4h","output_index":0,"sequence_number":68}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" mean","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"iEoAbAAy5dQ","output_index":0,"sequence_number":69}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" invent","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ELQYNFOF4","output_index":0,"sequence_number":70}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"ing","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"c9S0EIus0bjBk","output_index":0,"sequence_number":71}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" or","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zFOwG7sjVX8cZ","output_index":0,"sequence_number":72}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" lying","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"kLOSno5hAZ","output_index":0,"sequence_number":73}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" about","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"sZW682cjzl","output_index":0,"sequence_number":74}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" it","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"5SdVpOpP3tDW9","output_index":0,"sequence_number":75}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".\n\n","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"jIJkdpLZee7yv","output_index":0,"sequence_number":76}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"2","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"nPIBCntK2ClgdQs","output_index":0,"sequence_number":77}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"BzMXERtY6UTcark","output_index":0,"sequence_number":78}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" **","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Gk753o2HBcSud","output_index":0,"sequence_number":79}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Sur","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"UCUX6DSgEibpa","output_index":0,"sequence_number":80}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"prise","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"P9oQNuV01zl","output_index":0,"sequence_number":81}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" Element","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"qBups9bc","output_index":0,"sequence_number":82}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"**","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Z9dIdjqTsefoUa","output_index":0,"sequence_number":83}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":":","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"qm08Sch66EBWq9k","output_index":0,"sequence_number":84}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" J","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"J9bucKcls8A7M6","output_index":0,"sequence_number":85}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"okes","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"waZa21wHngIb","output_index":0,"sequence_number":86}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" often","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"VFnDaAMga6","output_index":0,"sequence_number":87}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" rely","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"YAFlPgnPcJC","output_index":0,"sequence_number":88}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" on","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"lLGSFHXK52aiW","output_index":0,"sequence_number":89}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"T7x2svQFyo3BjR","output_index":0,"sequence_number":90}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" setup","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ZMt6PMeCWr","output_index":0,"sequence_number":91}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" that","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"8l1qJa3KTEX","output_index":0,"sequence_number":92}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" leads","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zhhqrWIZAm","output_index":0,"sequence_number":93}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"yWdpvincjoJy","output_index":0,"sequence_number":94}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" audience","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"0ozlgo3","output_index":0,"sequence_number":95}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" to","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"S1HPNJAwEcewT","output_index":0,"sequence_number":96}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" expect","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"8KjGDm8mT","output_index":0,"sequence_number":97}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" one","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"XXmBZEjiFMNK","output_index":0,"sequence_number":98}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" thing","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zmoaWMkdXD","output_index":0,"sequence_number":99}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"HJoNcrcVeIKLodt","output_index":0,"sequence_number":100}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" only","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"fCI023RmwwQ","output_index":0,"sequence_number":101}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" to","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"2Zsh2cdqDmHB8","output_index":0,"sequence_number":102}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" deliver","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Hu5TXO23","output_index":0,"sequence_number":103}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" an","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"VZuZDgkAFfI1d","output_index":0,"sequence_number":104}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" unexpected","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"XZdrj","output_index":0,"sequence_number":105}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" punch","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"YwFnYN01eH","output_index":0,"sequence_number":106}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"line","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"iR5aKzuGEseR","output_index":0,"sequence_number":107}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"kSY2QLPXpQKhhD7","output_index":0,"sequence_number":108}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" Here","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"3r3xEOpBXyF","output_index":0,"sequence_number":109}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"F69vhN3jEtN497d","output_index":0,"sequence_number":110}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"dySiTv3oGlxo","output_index":0,"sequence_number":111}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" punch","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"NCRSrY6Eb5","output_index":0,"sequence_number":112}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"line","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"cY6NHRaYJHx0","output_index":0,"sequence_number":113}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" plays","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"VPEZBBm0Hh","output_index":0,"sequence_number":114}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" with","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"eF3lZXVH1To","output_index":0,"sequence_number":115}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" our","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"GZ348T5reB6D","output_index":0,"sequence_number":116}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" understanding","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"j6","output_index":0,"sequence_number":117}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"PavNXetPHc38s","output_index":0,"sequence_number":118}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" language","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Wj2Mv0J","output_index":0,"sequence_number":119}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"mWAw8s19WeQnY6i","output_index":0,"sequence_number":120}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" catching","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"3jyf8Cc","output_index":0,"sequence_number":121}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"J0L0wwVuGgxF","output_index":0,"sequence_number":122}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" listener","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"S2Vnlgk","output_index":0,"sequence_number":123}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" off","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"NtUUpay2a64F","output_index":0,"sequence_number":124}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" guard","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"b0wp7OyGDX","output_index":0,"sequence_number":125}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".\n\n","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"YKTvffawS9ptn","output_index":0,"sequence_number":126}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"3","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"NzNDjdBJrz4ag81","output_index":0,"sequence_number":127}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"rjI3dk1wGFtYDBd","output_index":0,"sequence_number":128}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" **","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"8WnxSsuSFODHO","output_index":0,"sequence_number":129}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Rel","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"BhV12AQZ9qmT2","output_index":0,"sequence_number":130}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"atable","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"UTzXf0v3oH","output_index":0,"sequence_number":131}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" Knowledge","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"qZOZIo","output_index":0,"sequence_number":132}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"**","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"cJm6vlGXwyzZXy","output_index":0,"sequence_number":133}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":":","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"dNoUfruWzSEiGbh","output_index":0,"sequence_number":134}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" The","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"9biJGwkcf8DT","output_index":0,"sequence_number":135}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" joke","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Fc2ayZORxSk","output_index":0,"sequence_number":136}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" uses","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"I2yi0U5MA3a","output_index":0,"sequence_number":137}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" common","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"0u1MaStc6","output_index":0,"sequence_number":138}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" knowledge","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"IRlavB","output_index":0,"sequence_number":139}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" about","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"CbPPGMmDGP","output_index":0,"sequence_number":140}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" science","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"s5Vc9kMd","output_index":0,"sequence_number":141}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" (","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"4aUXFyZztDOb20","output_index":0,"sequence_number":142}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"atoms","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"DwBfSdw5Z3T","output_index":0,"sequence_number":143}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":")","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"gdKE9yfh3BfiOk8","output_index":0,"sequence_number":144}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" in","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"lcnGy3TQDzeBy","output_index":0,"sequence_number":145}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"7sx3DNuKWmMa7t","output_index":0,"sequence_number":146}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" light","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"6LZkpgf4xU","output_index":0,"sequence_number":147}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"hearted","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"AvS1EEdHW","output_index":0,"sequence_number":148}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" way","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"h0NWSBAWvBOV","output_index":0,"sequence_number":149}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Cbi5mDUOpI44h46","output_index":0,"sequence_number":150}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" allowing","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"715Tb92","output_index":0,"sequence_number":151}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" it","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Yg9uD6tBhUwFO","output_index":0,"sequence_number":152}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" to","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"tNVbx8ZDFQ8SY","output_index":0,"sequence_number":153}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" resonate","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"gUJhGv2","output_index":0,"sequence_number":154}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" with","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"AgivlEZAqmk","output_index":0,"sequence_number":155}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"lXG5SHj7QhLL1s","output_index":0,"sequence_number":156}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" wide","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"b0BP9ORJI2X","output_index":0,"sequence_number":157}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" audience","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"zMj6fOG","output_index":0,"sequence_number":158}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".\n\n","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"Agq84NjYCn4xs","output_index":0,"sequence_number":159}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"These","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"dof54LQG7uE","output_index":0,"sequence_number":160}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" elements","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"1oWvGIK","output_index":0,"sequence_number":161}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" combine","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"kvuq0yp6","output_index":0,"sequence_number":162}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" to","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"SEn7dk277XYB5","output_index":0,"sequence_number":163}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" create","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"hyGSspNs9","output_index":0,"sequence_number":164}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"cO1mGkek487Zem","output_index":0,"sequence_number":165}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" playful","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"kJJQB4N6","output_index":0,"sequence_number":166}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" twist","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"CTJ0Ri1sOS","output_index":0,"sequence_number":167}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" that","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"xFCmJyq5ghR","output_index":0,"sequence_number":168}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" el","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"INwzSkCCOVkWg","output_index":0,"sequence_number":169}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"icits","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"9rgQQMWSwBj","output_index":0,"sequence_number":170}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" laughter","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"ymfcFY8","output_index":0,"sequence_number":171}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"!","item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"obfuscation":"QOWTZahcZGIHoZB","output_index":0,"sequence_number":172}
event: response.output_text.done
data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","logprobs":[],"output_index":0,"sequence_number":173,"text":"This joke is funny for a couple of reasons:\n\n1. **Wordplay**: The humor comes from the double meaning of the phrase \"make up.\" In one sense, atoms are the basic building blocks of matter and literally \"make up\" everything in the physical world. In another sense, \"making up\" something can mean inventing or lying about it.\n\n2. **Surprise Element**: Jokes often rely on a setup that leads the audience to expect one thing, only to deliver an unexpected punchline. Here, the punchline plays with our understanding of language, catching the listener off guard.\n\n3. **Relatable Knowledge**: The joke uses common knowledge about science (atoms) in a lighthearted way, allowing it to resonate with a wide audience.\n\nThese elements combine to create a playful twist that elicits laughter!"}
event: response.content_part.done
data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"This joke is funny for a couple of reasons:\n\n1. **Wordplay**: The humor comes from the double meaning of the phrase \"make up.\" In one sense, atoms are the basic building blocks of matter and literally \"make up\" everything in the physical world. In another sense, \"making up\" something can mean inventing or lying about it.\n\n2. **Surprise Element**: Jokes often rely on a setup that leads the audience to expect one thing, only to deliver an unexpected punchline. Here, the punchline plays with our understanding of language, catching the listener off guard.\n\n3. **Relatable Knowledge**: The joke uses common knowledge about science (atoms) in a lighthearted way, allowing it to resonate with a wide audience.\n\nThese elements combine to create a playful twist that elicits laughter!"},"sequence_number":174}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"msg_0108ce40c6fb22bd00695fa11416548197bd5b43b5a507d23d","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This joke is funny for a couple of reasons:\n\n1. **Wordplay**: The humor comes from the double meaning of the phrase \"make up.\" In one sense, atoms are the basic building blocks of matter and literally \"make up\" everything in the physical world. In another sense, \"making up\" something can mean inventing or lying about it.\n\n2. **Surprise Element**: Jokes often rely on a setup that leads the audience to expect one thing, only to deliver an unexpected punchline. Here, the punchline plays with our understanding of language, catching the listener off guard.\n\n3. **Relatable Knowledge**: The joke uses common knowledge about science (atoms) in a lighthearted way, allowing it to resonate with a wide audience.\n\nThese elements combine to create a playful twist that elicits laughter!"}],"role":"assistant"},"output_index":0,"sequence_number":175}
event: error
data: {"type":"error","error":{"type":"invalid_request_error","code":null,"message":"Conversation with id 'conv_695fa1132770819795d013275c77e8380108ce40c6fb22bd' not found.","param":null},"sequence_number":177}
@@ -0,0 +1,54 @@
-- request --
{
"input": "Use the code_exec tool to print hello world to the console.",
"model": "gpt-5",
"stream": true,
"tools": [
{
"type": "custom",
"name": "code_exec",
"description": "Executes arbitrary Python code."
}
]
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_0c26996bc41c2a0500696942e83634819fb71b2b8ff8a4a76c","object":"response","created_at":1768506088,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5-2025-08-07","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"custom","description":"Executes arbitrary Python code.","format":{"type":"text"},"name":"code_exec"}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_0c26996bc41c2a0500696942e83634819fb71b2b8ff8a4a76c","object":"response","created_at":1768506088,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5-2025-08-07","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"custom","description":"Executes arbitrary Python code.","format":{"type":"text"},"name":"code_exec"}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"rs_0c26996bc41c2a0500696942e8ae90819fb421c1b6a945aa99","type":"reasoning","summary":[]},"output_index":0,"sequence_number":2}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"rs_0c26996bc41c2a0500696942e8ae90819fb421c1b6a945aa99","type":"reasoning","summary":[]},"output_index":0,"sequence_number":3}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","type":"custom_tool_call","status":"in_progress","call_id":"call_2gSnF58IEhXLwlbnqbm5XKMd","input":"","name":"code_exec"},"output_index":1,"sequence_number":4}
event: response.custom_tool_call_input.delta
data: {"type":"response.custom_tool_call_input.delta","delta":"print","item_id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","obfuscation":"sTDUEAHu5aJ","output_index":1,"sequence_number":5}
event: response.custom_tool_call_input.delta
data: {"type":"response.custom_tool_call_input.delta","delta":"(\"","item_id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","obfuscation":"qvFA5MbN9ZUnBH","output_index":1,"sequence_number":6}
event: response.custom_tool_call_input.delta
data: {"type":"response.custom_tool_call_input.delta","delta":"hello","item_id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","obfuscation":"rRrXgQDOuwG","output_index":1,"sequence_number":7}
event: response.custom_tool_call_input.delta
data: {"type":"response.custom_tool_call_input.delta","delta":" world","item_id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","obfuscation":"DwnJdEFXvZ","output_index":1,"sequence_number":8}
event: response.custom_tool_call_input.delta
data: {"type":"response.custom_tool_call_input.delta","delta":"\")","item_id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","obfuscation":"pEr2t8Vpv3Ij96","output_index":1,"sequence_number":9}
event: response.custom_tool_call_input.done
data: {"type":"response.custom_tool_call_input.done","input":"print(\"hello world\")","item_id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","output_index":1,"sequence_number":10}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","type":"custom_tool_call","status":"completed","call_id":"call_2gSnF58IEhXLwlbnqbm5XKMd","input":"print(\"hello world\")","name":"code_exec"},"output_index":1,"sequence_number":11}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_0c26996bc41c2a0500696942e83634819fb71b2b8ff8a4a76c","object":"response","created_at":1768506088,"status":"completed","background":false,"completed_at":1768506095,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5-2025-08-07","output":[{"id":"rs_0c26996bc41c2a0500696942e8ae90819fb421c1b6a945aa99","type":"reasoning","summary":[]},{"id":"ctc_0c26996bc41c2a0500696942ee6db8819fa6e841317eecbfb2","type":"custom_tool_call","status":"completed","call_id":"call_2gSnF58IEhXLwlbnqbm5XKMd","input":"print(\"hello world\")","name":"code_exec"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"custom","description":"Executes arbitrary Python code.","format":{"type":"text"},"name":"code_exec"}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":64,"input_tokens_details":{"cached_tokens":0},"output_tokens":340,"output_tokens_details":{"reasoning_tokens":320},"total_tokens":404},"user":null,"metadata":{}},"sequence_number":12}
@@ -0,0 +1,21 @@
-- request --
{
"input": "tell me a joke",
"model": "gpt-4o-mini",
"stream": true
}
-- streaming --
HTTP/2.0 429 Too Many Requests
Content-Length: 176
Content-Type: application/json
{
"error": {
"message": "Rate limit exceeded. Please try again in 20 seconds.",
"type": "rate_limit_error",
"param": null,
"code": "rate_limit_exceeded"
}
}
@@ -0,0 +1,94 @@
Two reasoning output items before a function_call.
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-4.1",
"stream": true,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_0c3fb28cfcf463a500695fa2f0239481a095ec6ce3dfe4d458","object":"response","created_at":1767875312,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_0c3fb28cfcf463a500695fa2f0239481a095ec6ce3dfe4d458","object":"response","created_at":1767875312,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","type":"reasoning","status":"in_progress","summary":[]},"output_index":0,"sequence_number":2}
event: response.reasoning_summary_part.added
data: {"type":"response.reasoning_summary_part.added","item_id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","output_index":0,"part":{"type":"summary_text","text":""},"summary_index":0,"sequence_number":3}
event: response.reasoning_summary_text.delta
data: {"type":"response.reasoning_summary_text.delta","item_id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","output_index":0,"summary_index":0,"delta":"The user wants to add 3 and 5. Let me call the add function.","sequence_number":4}
event: response.reasoning_summary_text.done
data: {"type":"response.reasoning_summary_text.done","item_id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","output_index":0,"summary_index":0,"text":"The user wants to add 3 and 5. Let me call the add function.","sequence_number":5}
event: response.reasoning_summary_part.done
data: {"type":"response.reasoning_summary_part.done","item_id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","output_index":0,"part":{"type":"summary_text","text":"The user wants to add 3 and 5. Let me call the add function."},"summary_index":0,"sequence_number":6}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","type":"reasoning","status":"completed","summary":[{"type":"summary_text","text":"The user wants to add 3 and 5. Let me call the add function."}]},"output_index":0,"sequence_number":7}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"rs_1aa7045a8b68fa5200695fa23e200082b29cf79998e58bf94e","type":"reasoning","status":"in_progress","summary":[]},"output_index":1,"sequence_number":8}
event: response.reasoning_summary_part.added
data: {"type":"response.reasoning_summary_part.added","item_id":"rs_1aa7045a8b68fa5200695fa23e200082b29cf79998e58bf94e","output_index":1,"part":{"type":"summary_text","text":""},"summary_index":0,"sequence_number":9}
event: response.reasoning_summary_text.delta
data: {"type":"response.reasoning_summary_text.delta","item_id":"rs_1aa7045a8b68fa5200695fa23e200082b29cf79998e58bf94e","output_index":1,"summary_index":0,"delta":"After adding, I will check if the result is prime.","sequence_number":10}
event: response.reasoning_summary_text.done
data: {"type":"response.reasoning_summary_text.done","item_id":"rs_1aa7045a8b68fa5200695fa23e200082b29cf79998e58bf94e","output_index":1,"summary_index":0,"text":"After adding, I will check if the result is prime.","sequence_number":11}
event: response.reasoning_summary_part.done
data: {"type":"response.reasoning_summary_part.done","item_id":"rs_1aa7045a8b68fa5200695fa23e200082b29cf79998e58bf94e","output_index":1,"part":{"type":"summary_text","text":"After adding, I will check if the result is prime."},"summary_index":0,"sequence_number":12}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"rs_1aa7045a8b68fa5200695fa23e200082b29cf79998e58bf94e","type":"reasoning","status":"completed","summary":[{"type":"summary_text","text":"After adding, I will check if the result is prime."}]},"output_index":1,"sequence_number":13}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","type":"function_call","status":"in_progress","arguments":"","call_id":"call_7VaiUXZYuuuwWwviCrckxq6t","name":"add"},"output_index":2,"sequence_number":14}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"{\"a\":3,\"b\":5}","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","obfuscation":"gWZHP8i4lSgQYT","output_index":2,"sequence_number":15}
event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","arguments":"{\"a\":3,\"b\":5}","item_id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","output_index":2,"sequence_number":16}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_7VaiUXZYuuuwWwviCrckxq6t","name":"add"},"output_index":2,"sequence_number":17}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_0c3fb28cfcf463a500695fa2f0239481a095ec6ce3dfe4d458","object":"response","created_at":1767875312,"status":"completed","background":false,"completed_at":1767875312,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[{"id":"rs_0c3fb28cfcf463a500695fa2f0a0a881a0890103ba88b0628e","type":"reasoning","status":"completed","summary":[{"type":"summary_text","text":"The user wants to add 3 and 5. Let me call the add function."}]},{"id":"rs_1aa7045a8b68fa5200695fa23e200082b29cf79998e58bf94e","type":"reasoning","status":"completed","summary":[{"type":"summary_text","text":"After adding, I will check if the result is prime."}]},{"id":"fc_0c3fb28cfcf463a500695fa2f0b0a881a0890103ba88b0628e","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_7VaiUXZYuuuwWwviCrckxq6t","name":"add"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":58,"input_tokens_details":{"cached_tokens":0},"output_tokens":18,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":76},"user":null,"metadata":{}},"sequence_number":18}
@@ -0,0 +1,576 @@
-- request --
{
"input": "explain why this is funny.",
"model": "gpt-4o-mini",
"previous_response_id": "resp_0f9c4b2f224d858000695fa062bf048197a680f357bbb09000",
"stream": true
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_0f9c4b2f224d858000695fa0649b8c8197b38914b15a7add0e","object":"response","created_at":1767874660,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":"resp_0f9c4b2f224d858000695fa062bf048197a680f357bbb09000","prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_0f9c4b2f224d858000695fa0649b8c8197b38914b15a7add0e","object":"response","created_at":1767874660,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":"resp_0f9c4b2f224d858000695fa062bf048197a680f357bbb09000","prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2}
event: response.content_part.added
data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"The","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"DHEzS6FGVUr5E","output_index":0,"sequence_number":4}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" joke","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"QHJlLKd1i4I","output_index":0,"sequence_number":5}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"OUQeCkINJ5VDR","output_index":0,"sequence_number":6}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" funny","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"edUq2nh7rM","output_index":0,"sequence_number":7}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" because","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"lfIvyMYF","output_index":0,"sequence_number":8}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" it","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"IevxLSVnUQUv1","output_index":0,"sequence_number":9}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" uses","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"WCP3pFvqO6f","output_index":0,"sequence_number":10}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"Q5qCDtvROr5ZP0","output_index":0,"sequence_number":11}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" play","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"uYCIUmPmOxY","output_index":0,"sequence_number":12}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" on","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"eDN8BZywTMbfE","output_index":0,"sequence_number":13}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" words","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"m9d5ApPbls","output_index":0,"sequence_number":14}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"tZo36JrN5e2844D","output_index":0,"sequence_number":15}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" which","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"CVRHFumykU","output_index":0,"sequence_number":16}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"rdAYifDkSO66w","output_index":0,"sequence_number":17}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"qdkX1IGsZFixdS","output_index":0,"sequence_number":18}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" common","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"wqcOXveYt","output_index":0,"sequence_number":19}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" form","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"TkeTQ4v6hWr","output_index":0,"sequence_number":20}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"D38VdvUE7l0H9","output_index":0,"sequence_number":21}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" humor","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"iGyDNUGr0C","output_index":0,"sequence_number":22}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"cutbtYnZfT0n4JO","output_index":0,"sequence_number":23}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" \n\n","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"AnxZS7kyw6A9j","output_index":0,"sequence_number":24}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"1","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"RzSDkMTUnlSn0MZ","output_index":0,"sequence_number":25}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"5QY6AzdMey52NAl","output_index":0,"sequence_number":26}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" **","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"IfJewJwbvV84B","output_index":0,"sequence_number":27}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Double","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"d1QfJAfDG1","output_index":0,"sequence_number":28}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" Meaning","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"uUtusErd","output_index":0,"sequence_number":29}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"**","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"eEynq2ECHVNFHD","output_index":0,"sequence_number":30}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":":","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"KFnQwxpnVwbMrCS","output_index":0,"sequence_number":31}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" The","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"EmahvP8dVtog","output_index":0,"sequence_number":32}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" phrase","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"vWNyEuOHx","output_index":0,"sequence_number":33}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" \"","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"lAqrd6cYAXlhCz","output_index":0,"sequence_number":34}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"out","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"M2xl0znKS7ci1","output_index":0,"sequence_number":35}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"standing","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"e7X0kd8A","output_index":0,"sequence_number":36}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" in","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"ghB38DUHuwyZv","output_index":0,"sequence_number":37}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" his","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"T53kggqnrHeK","output_index":0,"sequence_number":38}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" field","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"jc98KS0TBP","output_index":0,"sequence_number":39}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"\"","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"vYewPc6Rn7twA59","output_index":0,"sequence_number":40}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" can","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"89reGpcrNM4F","output_index":0,"sequence_number":41}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" be","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"b5CoQSqeiPpDZ","output_index":0,"sequence_number":42}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" interpreted","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"K9js","output_index":0,"sequence_number":43}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" literally","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"weYNMB","output_index":0,"sequence_number":44}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"dkNP1549QnPgaK5","output_index":0,"sequence_number":45}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" meaning","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"smEFitne","output_index":0,"sequence_number":46}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"zKo3ymbuz2f3","output_index":0,"sequence_number":47}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" scare","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"3R7vsK0FsP","output_index":0,"sequence_number":48}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"crow","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"4f59ggc8KAOe","output_index":0,"sequence_number":49}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"c6MBXeF3KPdZ9","output_index":0,"sequence_number":50}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" literally","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"fMSP1r","output_index":0,"sequence_number":51}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" standing","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"ka1O1zO","output_index":0,"sequence_number":52}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" out","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"OxpPkKaOI4gI","output_index":0,"sequence_number":53}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" in","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"zKfYV5jEfCzt7","output_index":0,"sequence_number":54}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"KJg3i2F6LFQxzp","output_index":0,"sequence_number":55}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" field","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"HfFZ4RRe3f","output_index":0,"sequence_number":56}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" (","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"pQ4oXqVqV36gE0","output_index":0,"sequence_number":57}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"as","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"8SaeYXxOQU3cnd","output_index":0,"sequence_number":58}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" that's","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"MKgo8fAnG","output_index":0,"sequence_number":59}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" where","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"2fo6SoMB7u","output_index":0,"sequence_number":60}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" scare","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"HNfJHQO7Lu","output_index":0,"sequence_number":61}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"c","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"tJm1UVUt453MlZC","output_index":0,"sequence_number":62}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"rows","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"boBkPXPM6PM0","output_index":0,"sequence_number":63}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" are","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"4wv4vIp7bnqT","output_index":0,"sequence_number":64}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" found","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"7jbVDFFDrR","output_index":0,"sequence_number":65}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":").","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"iPVX4f8Nk2R36u","output_index":0,"sequence_number":66}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" However","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"WXD8NM59","output_index":0,"sequence_number":67}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"0zylfpXdumQWL3A","output_index":0,"sequence_number":68}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" it","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"r21NPwPwh6gWv","output_index":0,"sequence_number":69}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" also","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"yBuwgjQM3TS","output_index":0,"sequence_number":70}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" has","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"bKu6Uq5lPnBt","output_index":0,"sequence_number":71}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"UqLYVw32sivCxo","output_index":0,"sequence_number":72}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" figur","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"D9R8bxIy42","output_index":0,"sequence_number":73}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"ative","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"VPMseVGqlG2","output_index":0,"sequence_number":74}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" meaning","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"qKBa0orJ","output_index":0,"sequence_number":75}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":":","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"eXIpmNUtluw8Kvs","output_index":0,"sequence_number":76}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" it","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"1VBnyXJquHKL3","output_index":0,"sequence_number":77}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" suggests","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"b7tCjGH","output_index":0,"sequence_number":78}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" that","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"a0OorLr8zoQ","output_index":0,"sequence_number":79}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" someone","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"ihsOjyxt","output_index":0,"sequence_number":80}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"li0qLt2sYBmxJ","output_index":0,"sequence_number":81}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" exceptionally","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"FE","output_index":0,"sequence_number":82}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" skilled","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"v9HhHkN0","output_index":0,"sequence_number":83}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" or","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"mRkKQtBPBkrFb","output_index":0,"sequence_number":84}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" accomplished","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"cul","output_index":0,"sequence_number":85}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" in","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"3MJtuI4xfHA14","output_index":0,"sequence_number":86}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" their","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"rfRTP1G1LR","output_index":0,"sequence_number":87}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" area","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"IoFxhHT0S2D","output_index":0,"sequence_number":88}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"8ocFOGBmBxLAy","output_index":0,"sequence_number":89}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" expertise","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"MsxIJs","output_index":0,"sequence_number":90}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".\n\n","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"0hXVHSxmEzAfo","output_index":0,"sequence_number":91}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"2","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"kYR0FdWcxaVIyoT","output_index":0,"sequence_number":92}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"8AVkzTH5oQ2Ea3w","output_index":0,"sequence_number":93}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" **","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"uSEIHZyUCn6Ns","output_index":0,"sequence_number":94}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Sur","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"P73cMx6kWmrpf","output_index":0,"sequence_number":95}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"prise","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"3x0V86slZfc","output_index":0,"sequence_number":96}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" Element","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"P54ucKKE","output_index":0,"sequence_number":97}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"**","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"Y4gTEKEAXxQd5Z","output_index":0,"sequence_number":98}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":":","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"mb4rbxmph7FBfFY","output_index":0,"sequence_number":99}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" The","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"WOQucBmTB3W1","output_index":0,"sequence_number":100}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" punch","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"dh6riwNrDQ","output_index":0,"sequence_number":101}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"line","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"dG8x2aWeLBvy","output_index":0,"sequence_number":102}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" delivers","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"AvywpI0","output_index":0,"sequence_number":103}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" an","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"x7bDi4kmePshO","output_index":0,"sequence_number":104}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" unexpected","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"aa13X","output_index":0,"sequence_number":105}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" twist","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"5vWJPzoyXJ","output_index":0,"sequence_number":106}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"I4SgVqsdgh4Iq9y","output_index":0,"sequence_number":107}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" You","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"QmG22ploL4PA","output_index":0,"sequence_number":108}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" expect","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"d7pmncL1I","output_index":0,"sequence_number":109}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"DE3zEEd48D60","output_index":0,"sequence_number":110}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" award","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"9emuHJ8kzC","output_index":0,"sequence_number":111}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" to","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"zLlgDWd6XZnBI","output_index":0,"sequence_number":112}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" be","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"IofL9iR1fZWH7","output_index":0,"sequence_number":113}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" for","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"uZbOQUgwCQNS","output_index":0,"sequence_number":114}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" some","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"VdOVg200trS","output_index":0,"sequence_number":115}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" human","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"ZR1jijs6RR","output_index":0,"sequence_number":116}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" trait","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"YFiuWDRVqT","output_index":0,"sequence_number":117}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"yfYVyWUTwDCOlng","output_index":0,"sequence_number":118}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" but","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"fezlQ9HKgG29","output_index":0,"sequence_number":119}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" it's","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"kOKjHhMKvxo","output_index":0,"sequence_number":120}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" actually","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"8OzqVUl","output_index":0,"sequence_number":121}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"7ElfyBZnK0yTdq","output_index":0,"sequence_number":122}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" humorous","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"3hWMHah","output_index":0,"sequence_number":123}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" observation","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"eJyp","output_index":0,"sequence_number":124}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" about","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"NzbrTnXscy","output_index":0,"sequence_number":125}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"vEh4ykDzVtjw","output_index":0,"sequence_number":126}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" scare","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"DxDYdByBKX","output_index":0,"sequence_number":127}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"crow","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"b6cTjeCsdgS9","output_index":0,"sequence_number":128}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"s","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"fA0DCqJ1zIPX7z","output_index":0,"sequence_number":129}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" existence","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"g60ZOk","output_index":0,"sequence_number":130}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".\n\n","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"Cy7j62pp0KmeC","output_index":0,"sequence_number":131}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"3","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"j2isSvjsvXEfLT8","output_index":0,"sequence_number":132}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"hwl3YJGsYuliUZc","output_index":0,"sequence_number":133}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" **","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"OW7wjSZuS9PUF","output_index":0,"sequence_number":134}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Abs","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"hGDaoSd3EyQi0","output_index":0,"sequence_number":135}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"urd","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"kzwdZb5gdRBUO","output_index":0,"sequence_number":136}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"ity","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"AGB4ZWKhdAmpl","output_index":0,"sequence_number":137}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"**","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"AQM9tjRdYuiDxU","output_index":0,"sequence_number":138}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":":","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"zkwYjpymmS54zLL","output_index":0,"sequence_number":139}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" The","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"2bpD1VPjVqT4","output_index":0,"sequence_number":140}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" idea","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"yJrTH0IE5EI","output_index":0,"sequence_number":141}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"2F9lKnywGkXeg","output_index":0,"sequence_number":142}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"DeHfaCfUZ3OFUD","output_index":0,"sequence_number":143}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" scare","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"XbHJOoxc2T","output_index":0,"sequence_number":144}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"crow","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"5KhIZhunW2MB","output_index":0,"sequence_number":145}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"CUjg4FXgNB6fW9T","output_index":0,"sequence_number":146}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" an","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"nppy6fsrODqdD","output_index":0,"sequence_number":147}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" in","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"9f3xNqHJ31DbK","output_index":0,"sequence_number":148}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"animate","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"x5WNWGnkw","output_index":0,"sequence_number":149}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" object","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"JMehZgCZL","output_index":0,"sequence_number":150}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"G4moFDLqPgXl2og","output_index":0,"sequence_number":151}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" receiving","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"usujJs","output_index":0,"sequence_number":152}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" an","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"7rqwpfzZZwmpe","output_index":0,"sequence_number":153}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" award","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"ld5vgi60uy","output_index":0,"sequence_number":154}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" adds","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"kErKYzpCcOX","output_index":0,"sequence_number":155}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" an","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"1f6bhXZSy1GeE","output_index":0,"sequence_number":156}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" element","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"33nyGp9n","output_index":0,"sequence_number":157}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" of","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"YIa5Wv8NUAeAT","output_index":0,"sequence_number":158}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" absurd","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"s1Dxhug3I","output_index":0,"sequence_number":159}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"ity","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"RybQeNxIszXqy","output_index":0,"sequence_number":160}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"SKxMJyTX66sfon9","output_index":0,"sequence_number":161}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" making","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"SAXT80cOM","output_index":0,"sequence_number":162}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" it","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"tzZHDUqVepH96","output_index":0,"sequence_number":163}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" more","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"8qRMxic0p2b","output_index":0,"sequence_number":164}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" amusing","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"Zb7GsyKt","output_index":0,"sequence_number":165}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":".\n\n","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"31laY4QlnMB6y","output_index":0,"sequence_number":166}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Overall","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"95bVDR9T0","output_index":0,"sequence_number":167}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"OhUixHaPQ5ebUzy","output_index":0,"sequence_number":168}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" it's","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"bbYLkiw2T8E","output_index":0,"sequence_number":169}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"ostR0cxyGIJD","output_index":0,"sequence_number":170}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" clever","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"PpGqKElOs","output_index":0,"sequence_number":171}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" word","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"I0DETY9xxgm","output_index":0,"sequence_number":172}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"play","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"6zWRZleG0DvD","output_index":0,"sequence_number":173}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" combined","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"buIFOKO","output_index":0,"sequence_number":174}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" with","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"32zyLmemqJP","output_index":0,"sequence_number":175}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" an","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"Ua7JQewv7wBMa","output_index":0,"sequence_number":176}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" unexpected","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"sFOzn","output_index":0,"sequence_number":177}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" twist","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"2VbhR1bqcr","output_index":0,"sequence_number":178}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" that","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"F7jlTqm5mqb","output_index":0,"sequence_number":179}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" makes","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"Ywx6KbSzzU","output_index":0,"sequence_number":180}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"B4aGSKflNN22","output_index":0,"sequence_number":181}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" joke","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"hNMEMTZL5Ja","output_index":0,"sequence_number":182}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" effective","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"bsB12A","output_index":0,"sequence_number":183}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"!","item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"obfuscation":"pjObCPZ3LfG6WVF","output_index":0,"sequence_number":184}
event: response.output_text.done
data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","logprobs":[],"output_index":0,"sequence_number":185,"text":"The joke is funny because it uses a play on words, which is a common form of humor. \n\n1. **Double Meaning**: The phrase \"outstanding in his field\" can be interpreted literally, meaning the scarecrow is literally standing out in a field (as that's where scarecrows are found). However, it also has a figurative meaning: it suggests that someone is exceptionally skilled or accomplished in their area of expertise.\n\n2. **Surprise Element**: The punchline delivers an unexpected twist. You expect the award to be for some human trait, but it's actually a humorous observation about the scarecrows existence.\n\n3. **Absurdity**: The idea of a scarecrow, an inanimate object, receiving an award adds an element of absurdity, making it more amusing.\n\nOverall, it's the clever wordplay combined with an unexpected twist that makes the joke effective!"}
event: response.content_part.done
data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"The joke is funny because it uses a play on words, which is a common form of humor. \n\n1. **Double Meaning**: The phrase \"outstanding in his field\" can be interpreted literally, meaning the scarecrow is literally standing out in a field (as that's where scarecrows are found). However, it also has a figurative meaning: it suggests that someone is exceptionally skilled or accomplished in their area of expertise.\n\n2. **Surprise Element**: The punchline delivers an unexpected twist. You expect the award to be for some human trait, but it's actually a humorous observation about the scarecrows existence.\n\n3. **Absurdity**: The idea of a scarecrow, an inanimate object, receiving an award adds an element of absurdity, making it more amusing.\n\nOverall, it's the clever wordplay combined with an unexpected twist that makes the joke effective!"},"sequence_number":186}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The joke is funny because it uses a play on words, which is a common form of humor. \n\n1. **Double Meaning**: The phrase \"outstanding in his field\" can be interpreted literally, meaning the scarecrow is literally standing out in a field (as that's where scarecrows are found). However, it also has a figurative meaning: it suggests that someone is exceptionally skilled or accomplished in their area of expertise.\n\n2. **Surprise Element**: The punchline delivers an unexpected twist. You expect the award to be for some human trait, but it's actually a humorous observation about the scarecrows existence.\n\n3. **Absurdity**: The idea of a scarecrow, an inanimate object, receiving an award adds an element of absurdity, making it more amusing.\n\nOverall, it's the clever wordplay combined with an unexpected twist that makes the joke effective!"}],"role":"assistant"},"output_index":0,"sequence_number":187}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_0f9c4b2f224d858000695fa0649b8c8197b38914b15a7add0e","object":"response","created_at":1767874660,"status":"completed","background":false,"completed_at":1767874663,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0f9c4b2f224d858000695fa064f1dc81979e4a37fab905af69","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The joke is funny because it uses a play on words, which is a common form of humor. \n\n1. **Double Meaning**: The phrase \"outstanding in his field\" can be interpreted literally, meaning the scarecrow is literally standing out in a field (as that's where scarecrows are found). However, it also has a figurative meaning: it suggests that someone is exceptionally skilled or accomplished in their area of expertise.\n\n2. **Surprise Element**: The punchline delivers an unexpected twist. You expect the award to be for some human trait, but it's actually a humorous observation about the scarecrows existence.\n\n3. **Absurdity**: The idea of a scarecrow, an inanimate object, receiving an award adds an element of absurdity, making it more amusing.\n\nOverall, it's the clever wordplay combined with an unexpected twist that makes the joke effective!"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":"resp_0f9c4b2f224d858000695fa062bf048197a680f357bbb09000","prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":43,"input_tokens_details":{"cached_tokens":0},"output_tokens":182,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":225},"user":null,"metadata":{}},"sequence_number":188}
@@ -0,0 +1,83 @@
-- request --
{
"input": "tell me a joke",
"model": "gpt-4o-mini",
"stream": true
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_0f9c4b2f224d858000695fa062bf048197a680f357bbb09000","object":"response","created_at":1767874658,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_0f9c4b2f224d858000695fa062bf048197a680f357bbb09000","object":"response","created_at":1767874658,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2}
event: response.content_part.added
data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Why","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"N16SG5UiLncOU","output_index":0,"sequence_number":4}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" did","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"OpojJ3pv0h55","output_index":0,"sequence_number":5}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" the","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"11RCrnBxLo5x","output_index":0,"sequence_number":6}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" scare","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"QZrRBlk6BV","output_index":0,"sequence_number":7}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"crow","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"gp7F8IVupiHG","output_index":0,"sequence_number":8}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" win","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"uKq4X8mT1jl9","output_index":0,"sequence_number":9}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" an","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"2Ox5JzaAsJHuT","output_index":0,"sequence_number":10}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" award","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"ZOQbZabNAQ","output_index":0,"sequence_number":11}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"?\n\n","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"N2dSd0FHBxooR","output_index":0,"sequence_number":12}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"Because","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"LZ1O4laHt","output_index":0,"sequence_number":13}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" he","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"dqcS6ePaMvxMD","output_index":0,"sequence_number":14}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" was","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"nR6CtC7MUsWW","output_index":0,"sequence_number":15}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" outstanding","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"dNVG","output_index":0,"sequence_number":16}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" in","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"P7w4jjOcdVOla","output_index":0,"sequence_number":17}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" his","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"u9dg4RLIld4e","output_index":0,"sequence_number":18}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":" field","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"qefuqzOCOy","output_index":0,"sequence_number":19}
event: response.output_text.delta
data: {"type":"response.output_text.delta","content_index":0,"delta":"!","item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"obfuscation":"DT9j4dSh0xyJdxU","output_index":0,"sequence_number":20}
event: response.output_text.done
data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","logprobs":[],"output_index":0,"sequence_number":21,"text":"Why did the scarecrow win an award?\n\nBecause he was outstanding in his field!"}
event: response.content_part.done
data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"Why did the scarecrow win an award?\n\nBecause he was outstanding in his field!"},"sequence_number":22}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Why did the scarecrow win an award?\n\nBecause he was outstanding in his field!"}],"role":"assistant"},"output_index":0,"sequence_number":23}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_0f9c4b2f224d858000695fa062bf048197a680f357bbb09000","object":"response","created_at":1767874658,"status":"completed","background":false,"completed_at":1767874660,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Why did the scarecrow win an award?\n\nBecause he was outstanding in his field!"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":11,"input_tokens_details":{"cached_tokens":0},"output_tokens":18,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":29},"user":null,"metadata":{}},"sequence_number":24}
@@ -0,0 +1,86 @@
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Also add 10 + 20. Use the add function for both."
}
],
"model": "gpt-4.1",
"stream": true,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_parallel_streaming_001","object":"response","created_at":1767875312,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_parallel_streaming_001","object":"response","created_at":1767875312,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"rs_parallel_streaming_reasoning_001","type":"reasoning","status":"in_progress","summary":[]},"output_index":0,"sequence_number":2}
event: response.reasoning_summary_part.added
data: {"type":"response.reasoning_summary_part.added","item_id":"rs_parallel_streaming_reasoning_001","output_index":0,"part":{"type":"summary_text","text":""},"summary_index":0,"sequence_number":3}
event: response.reasoning_summary_text.delta
data: {"type":"response.reasoning_summary_text.delta","item_id":"rs_parallel_streaming_reasoning_001","output_index":0,"summary_index":0,"delta":"The user wants two additions: 3+5 and 10+20. I'll call add for both.","sequence_number":4}
event: response.reasoning_summary_text.done
data: {"type":"response.reasoning_summary_text.done","item_id":"rs_parallel_streaming_reasoning_001","output_index":0,"summary_index":0,"text":"The user wants two additions: 3+5 and 10+20. I'll call add for both.","sequence_number":5}
event: response.reasoning_summary_part.done
data: {"type":"response.reasoning_summary_part.done","item_id":"rs_parallel_streaming_reasoning_001","output_index":0,"part":{"type":"summary_text","text":"The user wants two additions: 3+5 and 10+20. I'll call add for both."},"summary_index":0,"sequence_number":6}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"rs_parallel_streaming_reasoning_001","type":"reasoning","status":"completed","summary":[{"type":"summary_text","text":"The user wants two additions: 3+5 and 10+20. I'll call add for both."}]},"output_index":0,"sequence_number":7}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"fc_parallel_streaming_first_001","type":"function_call","status":"in_progress","arguments":"","call_id":"call_ParallelStreamFirst001","name":"add"},"output_index":1,"sequence_number":8}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"{\"a\":3,\"b\":5}","item_id":"fc_parallel_streaming_first_001","output_index":1,"sequence_number":9}
event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","arguments":"{\"a\":3,\"b\":5}","item_id":"fc_parallel_streaming_first_001","output_index":1,"sequence_number":10}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"fc_parallel_streaming_first_001","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_ParallelStreamFirst001","name":"add"},"output_index":1,"sequence_number":11}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"fc_parallel_streaming_second_001","type":"function_call","status":"in_progress","arguments":"","call_id":"call_ParallelStreamSecond01","name":"add"},"output_index":2,"sequence_number":12}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"{\"a\":10,\"b\":20}","item_id":"fc_parallel_streaming_second_001","output_index":2,"sequence_number":13}
event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","arguments":"{\"a\":10,\"b\":20}","item_id":"fc_parallel_streaming_second_001","output_index":2,"sequence_number":14}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"fc_parallel_streaming_second_001","type":"function_call","status":"completed","arguments":"{\"a\":10,\"b\":20}","call_id":"call_ParallelStreamSecond01","name":"add"},"output_index":2,"sequence_number":15}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_parallel_streaming_001","object":"response","created_at":1767875312,"status":"completed","background":false,"completed_at":1767875312,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","output":[{"id":"rs_parallel_streaming_reasoning_001","type":"reasoning","status":"completed","summary":[{"type":"summary_text","text":"The user wants two additions: 3+5 and 10+20. I'll call add for both."}]},{"id":"fc_parallel_streaming_first_001","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_ParallelStreamFirst001","name":"add"},{"id":"fc_parallel_streaming_second_001","type":"function_call","status":"completed","arguments":"{\"a\":10,\"b\":20}","call_id":"call_ParallelStreamSecond01","name":"add"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":65,"input_tokens_details":{"cached_tokens":0},"output_tokens":30,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":95},"user":null,"metadata":{}},"sequence_number":16}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,20 @@
-- request --
{
"input": "hello_stream_error",
"model": "gpt-6.7",
"stream": true
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_123","object":"response","status":"in_progress","error":null,"output":[]},"sequence_number":1}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_123","object":"response","status":"in_progress","error":null,"output":[]},"sequence_number":2}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_123","output_index":0,"content_index":0,"delta":"Hello","sequence_number":3}
event: error
data: {"type":"error","code":"ERR_SOMETHING","message":"Something went wrong","param":null,"sequence_number":4}
@@ -0,0 +1,20 @@
-- request --
{
"input": "hello_stream_failure",
"model": "gpt-6.7",
"stream": true
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_123","object":"response","status":"in_progress","error":null,"output":[]},"sequence_number":1}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_123","object":"response","status":"in_progress","error":null,"output":[]},"sequence_number":2}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_123","output_index":0,"content_index":0,"delta":"Hello","sequence_number":3}
event: response.failed
data: {"type":"response.failed","response":{"id":"resp_123","object":"response","status":"failed","error":{"code":"server_error","message":"The model failed to generate a response."},"output":[]},"sequence_number":4}
@@ -0,0 +1,94 @@
Both a reasoning summary and a commentary message before a function_call.
-- request --
{
"input": [
{
"role": "user",
"content": "Is 3 + 5 a prime number? Use the add function to calculate the sum."
}
],
"model": "gpt-5.4",
"stream": true,
"tools": [
{
"type": "function",
"name": "add",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_1bba3bc54ed351c41270c26831354d920fcc75088476e53de6","object":"response","created_at":1773229900,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.4-2026-03-05","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"xhigh","summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"low"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_1bba3bc54ed351c41270c26831354d920fcc75088476e53de6","object":"response","created_at":1773229900,"status":"in_progress","background":false,"completed_at":null,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.4-2026-03-05","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"xhigh","summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"low"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"rs_1bba3bc54ed351c41270c26831908d920fcc75088476e53de6","type":"reasoning","status":"in_progress","summary":[]},"output_index":0,"sequence_number":2}
event: response.reasoning_summary_part.added
data: {"type":"response.reasoning_summary_part.added","item_id":"rs_1bba3bc54ed351c41270c26831908d920fcc75088476e53de6","output_index":0,"part":{"type":"summary_text","text":""},"summary_index":0,"sequence_number":3}
event: response.reasoning_summary_text.delta
data: {"type":"response.reasoning_summary_text.delta","item_id":"rs_1bba3bc54ed351c41270c26831908d920fcc75088476e53de6","output_index":0,"summary_index":0,"delta":"I need to add 3 and 5 to check primality.","sequence_number":4}
event: response.reasoning_summary_text.done
data: {"type":"response.reasoning_summary_text.done","item_id":"rs_1bba3bc54ed351c41270c26831908d920fcc75088476e53de6","output_index":0,"summary_index":0,"text":"I need to add 3 and 5 to check primality.","sequence_number":5}
event: response.reasoning_summary_part.done
data: {"type":"response.reasoning_summary_part.done","item_id":"rs_1bba3bc54ed351c41270c26831908d920fcc75088476e53de6","output_index":0,"part":{"type":"summary_text","text":"I need to add 3 and 5 to check primality."},"summary_index":0,"sequence_number":6}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"rs_1bba3bc54ed351c41270c26831908d920fcc75088476e53de6","type":"reasoning","status":"completed","encrypted_content":"gAAAAA==","summary":[{"type":"summary_text","text":"I need to add 3 and 5 to check primality."}]},"output_index":0,"sequence_number":7}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"msg_1bba3bc54ed351c41270c26831a09d920fdd86199587f64ef7","type":"message","status":"in_progress","content":[],"phase":"commentary","role":"assistant"},"output_index":1,"sequence_number":8}
event: response.content_part.added
data: {"type":"response.content_part.added","item_id":"msg_1bba3bc54ed351c41270c26831a09d920fdd86199587f64ef7","output_index":1,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]},"sequence_number":9}
event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_1bba3bc54ed351c41270c26831a09d920fdd86199587f64ef7","output_index":1,"content_index":0,"delta":"Let me calculate the sum first using the add function.","sequence_number":10}
event: response.output_text.done
data: {"type":"response.output_text.done","item_id":"msg_1bba3bc54ed351c41270c26831a09d920fdd86199587f64ef7","output_index":1,"content_index":0,"text":"Let me calculate the sum first using the add function.","sequence_number":11}
event: response.content_part.done
data: {"type":"response.content_part.done","item_id":"msg_1bba3bc54ed351c41270c26831a09d920fdd86199587f64ef7","output_index":1,"content_index":0,"part":{"type":"output_text","text":"Let me calculate the sum first using the add function.","annotations":[]},"sequence_number":12}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"msg_1bba3bc54ed351c41270c26831a09d920fdd86199587f64ef7","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"text":"Let me calculate the sum first using the add function."}],"phase":"commentary","role":"assistant"},"output_index":1,"sequence_number":13}
event: response.output_item.added
data: {"type":"response.output_item.added","item":{"id":"fc_1bba3bc54ed351c41270c26831b0ad920fee97200698074f08","type":"function_call","status":"in_progress","arguments":"","call_id":"call_B9UjYX01Lvvv1XwjDsdmRW3f","name":"add"},"output_index":2,"sequence_number":14}
event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","delta":"{\"a\":3,\"b\":5}","item_id":"fc_1bba3bc54ed351c41270c26831b0ad920fee97200698074f08","output_index":2,"sequence_number":15}
event: response.function_call_arguments.done
data: {"type":"response.function_call_arguments.done","arguments":"{\"a\":3,\"b\":5}","item_id":"fc_1bba3bc54ed351c41270c26831b0ad920fee97200698074f08","output_index":2,"sequence_number":16}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"id":"fc_1bba3bc54ed351c41270c26831b0ad920fee97200698074f08","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_B9UjYX01Lvvv1XwjDsdmRW3f","name":"add"},"output_index":2,"sequence_number":17}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_1bba3bc54ed351c41270c26831354d920fcc75088476e53de6","object":"response","created_at":1773229900,"status":"completed","background":false,"completed_at":1773229905,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-5.4-2026-03-05","output":[{"id":"rs_1bba3bc54ed351c41270c26831908d920fcc75088476e53de6","type":"reasoning","status":"completed","encrypted_content":"gAAAAA==","summary":[{"type":"summary_text","text":"I need to add 3 and 5 to check primality."}]},{"id":"msg_1bba3bc54ed351c41270c26831a09d920fdd86199587f64ef7","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"text":"Let me calculate the sum first using the add function."}],"phase":"commentary","role":"assistant"},{"id":"fc_1bba3bc54ed351c41270c26831b0ad920fee97200698074f08","type":"function_call","status":"completed","arguments":"{\"a\":3,\"b\":5}","call_id":"call_B9UjYX01Lvvv1XwjDsdmRW3f","name":"add"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":"xhigh","summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"low"},"tool_choice":"auto","tools":[{"type":"function","description":"Add two numbers together.","name":"add","parameters":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":0.98,"truncation":"disabled","usage":{"input_tokens":58,"input_tokens_details":{"cached_tokens":0},"output_tokens":35,"output_tokens_details":{"reasoning_tokens":10},"total_tokens":93},"user":null,"metadata":{}},"sequence_number":18}
@@ -0,0 +1,21 @@
-- request --
{
"input": "hello_wrong_format",
"model": "gpt-6.7",
"stream": true
}
-- streaming --
event: response.created
data: {"type":"response.created","response":{"id":"resp_123","object":"response","status":"in_progress","error":null,"output":[]},"sequence_number":1}
event: response.in_progress
data: {"type":"response.in_progress","response":{"id":"resp_123","object":"response","status":"in_progress","error":null,"output":[]},"sequence_number":2}
event: response.output_text.delta
da
ta: { "wrong format": should be forwarded as received
event: response.completed
data: {"type":"response.completed","response":{"id":"resp_123","object":"response","created_at":1767874658,"status":"completed","background":false,"completed_at":1767874660,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0f9c4b2f224d858000695fa063d4708197af73c2f37cb0b9d3","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Why did the scarecrow win an award?\n\nBecause he was outstanding in his field!"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":11,"input_tokens_details":{"cached_tokens":0},"output_tokens":18,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":29},"user":null,"metadata":{}},"sequence_number":24}
+80
View File
@@ -0,0 +1,80 @@
package intercept
import (
"fmt"
"strings"
ant_option "github.com/anthropics/anthropic-sdk-go/option"
oai_option "github.com/openai/openai-go/v3/option"
"github.com/coder/coder/v2/aibridge/context"
)
const (
prefix = "X-AI-Bridge-Actor"
)
func ActorIDHeader() string {
return fmt.Sprintf("%s-ID", prefix)
}
func ActorMetadataHeader(name string) string {
return fmt.Sprintf("%s-Metadata-%s", prefix, name)
}
func IsActorHeader(name string) bool {
return strings.HasPrefix(strings.ToLower(name), strings.ToLower(prefix))
}
// ActorHeadersAsOpenAIOpts produces a slice of headers using OpenAI's RequestOption type.
func ActorHeadersAsOpenAIOpts(actor *context.Actor) []oai_option.RequestOption {
var opts []oai_option.RequestOption
headers := headersFromActor(actor)
if len(headers) == 0 {
return nil
}
for k, v := range headers {
// [k] will be canonicalized, see [http.Header]'s [Add] method.
opts = append(opts, oai_option.WithHeaderAdd(k, v))
}
return opts
}
// ActorHeadersAsAnthropicOpts produces a slice of headers using Anthropic's RequestOption type.
func ActorHeadersAsAnthropicOpts(actor *context.Actor) []ant_option.RequestOption {
var opts []ant_option.RequestOption
headers := headersFromActor(actor)
if len(headers) == 0 {
return nil
}
for k, v := range headers {
// [k] will be canonicalized, see [http.Header]'s [Add] method.
opts = append(opts, ant_option.WithHeaderAdd(k, v))
}
return opts
}
// headersFromActor produces a map of headers from a given [context.Actor].
func headersFromActor(actor *context.Actor) map[string]string {
if actor == nil {
return nil
}
headers := make(map[string]string, len(actor.Metadata)+1)
// Add actor ID.
headers[ActorIDHeader()] = actor.ID
// Add headers for provided metadata.
for k, v := range actor.Metadata {
headers[ActorMetadataHeader(k)] = fmt.Sprintf("%v", v)
}
return headers
}
+57
View File
@@ -0,0 +1,57 @@
package intercept_test
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/intercept"
"github.com/coder/coder/v2/aibridge/recorder"
)
func TestNilActor(t *testing.T) {
t.Parallel()
require.Nil(t, intercept.ActorHeadersAsOpenAIOpts(nil))
require.Nil(t, intercept.ActorHeadersAsAnthropicOpts(nil))
}
func TestBasic(t *testing.T) {
t.Parallel()
actorID := uuid.NewString()
actor := &context.Actor{
ID: actorID,
}
// We can't peek inside since these opts require an internal type to apply onto.
// All we can do is check the length.
// See TestActorHeaders for an integration test.
oaiOpts := intercept.ActorHeadersAsOpenAIOpts(actor)
require.Len(t, oaiOpts, 1)
antOpts := intercept.ActorHeadersAsAnthropicOpts(actor)
require.Len(t, antOpts, 1)
}
func TestBasicAndMetadata(t *testing.T) {
t.Parallel()
actorID := uuid.NewString()
actor := &context.Actor{
ID: actorID,
Metadata: recorder.Metadata{
"This": "That",
"And": "The other",
},
}
// We can't peek inside since these opts require an internal type to apply onto.
// All we can do is check the length.
// See TestActorHeaders for an integration test.
oaiOpts := intercept.ActorHeadersAsOpenAIOpts(actor)
require.Len(t, oaiOpts, 1+len(actor.Metadata))
antOpts := intercept.ActorHeadersAsAnthropicOpts(actor)
require.Len(t, antOpts, 1+len(actor.Metadata))
}
+290
View File
@@ -0,0 +1,290 @@
package apidump
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"slices"
"strings"
"github.com/google/uuid"
"github.com/tidwall/pretty"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/utils"
"github.com/coder/quartz"
)
const (
// SuffixRequest is the file suffix for request dump files.
SuffixRequest = ".req.txt"
// SuffixResponse is the file suffix for response dump files.
SuffixResponse = ".resp.txt"
// SuffixError is the file suffix for error dump files written when a request fails.
SuffixError = ".req_error.txt"
)
// MiddlewareNext is the function to call the next middleware or the actual request.
type MiddlewareNext = func(*http.Request) (*http.Response, error)
// Middleware is an HTTP middleware function compatible with SDK WithMiddleware options.
type Middleware = func(*http.Request, MiddlewareNext) (*http.Response, error)
// NewBridgeMiddleware returns a middleware function that dumps requests and responses to files.
// If baseDir is empty, returns nil (no middleware).
func NewBridgeMiddleware(baseDir string, provider string, model string, interceptionID uuid.UUID, logger slog.Logger, clk quartz.Clock) Middleware {
if baseDir == "" {
return nil
}
d := &dumper{
dumpPath: interceptDumpPath(baseDir, provider, model, interceptionID, clk),
logger: logger,
}
return func(req *http.Request, next MiddlewareNext) (*http.Response, error) {
if err := d.dumpRequest(req); err != nil {
logger.Named("apidump").Warn(req.Context(), "failed to dump request", slog.Error(err))
}
resp, err := next(req)
if err != nil {
if dumpErr := d.dumpError(err); dumpErr != nil {
logger.Named("apidump").Warn(req.Context(), "failed to dump request error", slog.Error(dumpErr))
}
return resp, err
}
if err := d.dumpResponse(resp); err != nil {
logger.Named("apidump").Warn(req.Context(), "failed to dump response", slog.Error(err))
}
return resp, nil
}
}
type dumper struct {
dumpPath string
logger slog.Logger
}
func (d *dumper) dumpRequest(req *http.Request) error {
dumpPath := d.dumpPath + SuffixRequest
if err := os.MkdirAll(filepath.Dir(dumpPath), 0o755); err != nil {
return xerrors.Errorf("create dump dir: %w", err)
}
// Read and restore body
var bodyBytes []byte
if req.Body != nil {
var err error
bodyBytes, err = io.ReadAll(req.Body)
if err != nil {
return xerrors.Errorf("read request body: %w", err)
}
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
}
prettyBody := prettyPrintJSON(bodyBytes)
// Build raw HTTP request format
var buf bytes.Buffer
_, err := fmt.Fprintf(&buf, "%s %s %s\r\n", req.Method, req.URL.RequestURI(), req.Proto)
if err != nil {
return xerrors.Errorf("write request uri: %w", err)
}
err = d.writeRedactedHeaders(&buf, req.Header, sensitiveRequestHeaders, map[string]string{
"Content-Length": fmt.Sprintf("%d", len(prettyBody)),
})
if err != nil {
return xerrors.Errorf("write request headers: %w", err)
}
_, err = fmt.Fprintf(&buf, "\r\n")
if err != nil {
return xerrors.Errorf("write request header terminator: %w", err)
}
// bytes.Buffer writes to in-memory storage and never return errors.
_, _ = buf.Write(prettyBody)
_ = buf.WriteByte('\n')
return os.WriteFile(dumpPath, buf.Bytes(), 0o644) //nolint:gosec // https://github.com/coder/aibridge/pull/256#discussion_r3072143983
}
func (d *dumper) dumpError(reqErr error) error {
dumpPath := d.dumpPath + SuffixError
if err := os.MkdirAll(filepath.Dir(dumpPath), 0o755); err != nil {
return xerrors.Errorf("create dump dir: %w", err)
}
return os.WriteFile(dumpPath, []byte(reqErr.Error()+"\n"), 0o644) //nolint:gosec // same rationale as other dump files
}
func (d *dumper) dumpResponse(resp *http.Response) error {
dumpPath := d.dumpPath + SuffixResponse
// Build raw HTTP response headers
var headerBuf bytes.Buffer
_, err := fmt.Fprintf(&headerBuf, "%s %s\r\n", resp.Proto, resp.Status)
if err != nil {
return xerrors.Errorf("write response status: %w", err)
}
err = d.writeRedactedHeaders(&headerBuf, resp.Header, sensitiveResponseHeaders, nil)
if err != nil {
return xerrors.Errorf("write response headers: %w", err)
}
_, err = fmt.Fprintf(&headerBuf, "\r\n")
if err != nil {
return xerrors.Errorf("write response header terminator: %w", err)
}
if resp.Body == nil {
// No body, just write headers
return os.WriteFile(dumpPath, headerBuf.Bytes(), 0o644) //nolint:gosec // https://github.com/coder/aibridge/pull/256#discussion_r3072143983
}
// Wrap the response body to capture it as it streams
resp.Body = &streamingBodyDumper{
body: resp.Body,
dumpPath: dumpPath,
headerData: headerBuf.Bytes(),
logger: func(err error) {
d.logger.Named("apidump").Warn(context.Background(), "failed to initialize response dump", slog.Error(err))
},
}
return nil
}
// writeRedactedHeaders writes HTTP headers in wire format (Key: Value\r\n) to w,
// redacting sensitive values and applying any overrides. Headers are sorted by key
// for deterministic output.
// `sensitive` and `overrides` must both supply keys in canonicalized form.
// See [textproto.MIMEHeader].
func (*dumper) writeRedactedHeaders(w io.Writer, headers http.Header, sensitive map[string]struct{}, overrides map[string]string) error {
// Collect all header keys including overrides.
headerKeys := make([]string, 0, len(headers)+len(overrides))
seen := make(map[string]struct{}, len(headers)+len(overrides))
for key := range headers {
headerKeys = append(headerKeys, key)
seen[key] = struct{}{}
}
// Add override keys that don't exist in headers.
for key := range overrides {
if _, ok := seen[key]; !ok {
headerKeys = append(headerKeys, key)
}
}
slices.Sort(headerKeys)
for _, key := range headerKeys {
_, isSensitive := sensitive[key]
values := headers[key]
// If no values exist but we have an override, use that.
if len(values) == 0 {
if override, ok := overrides[key]; ok {
_, err := fmt.Fprintf(w, "%s: %s\r\n", key, override)
if err != nil {
return xerrors.Errorf("write response header override: %w", err)
}
}
continue
}
for _, value := range values {
if override, ok := overrides[key]; ok {
value = override
}
if isSensitive {
value = utils.MaskSecret(value)
}
_, err := fmt.Fprintf(w, "%s: %s\r\n", key, value)
if err != nil {
return xerrors.Errorf("write response headers: %w", err)
}
}
}
return nil
}
// interceptDumpPath returns the base file path (without req/resp suffix) for an interception dump.
func interceptDumpPath(baseDir string, provider string, model string, interceptionID uuid.UUID, clk quartz.Clock) string {
safeModel := strings.ReplaceAll(model, "/", "-")
return filepath.Join(baseDir, provider, safeModel, fmt.Sprintf("%d-%s", clk.Now().UTC().UnixMilli(), interceptionID))
}
// passthroughDumpPath returns the base file path (without req/resp suffix) for a passthrough dump.
func passthroughDumpPath(baseDir string, provider string, urlPath string, clk quartz.Clock) string {
safeURLPath := strings.ReplaceAll(strings.TrimPrefix(urlPath, "/"), "/", "-")
return filepath.Join(baseDir, provider, "passthrough", fmt.Sprintf("%d-%s-%s", clk.Now().UTC().UnixMilli(), safeURLPath, uuid.NewString()[:4]))
}
// NewPassthroughMiddleware returns http.RoundTripper that dumps requests and responses to files.
// If baseDir is empty, returns the original transport unchanged.
// Used for logging in pass through routes.
func NewPassthroughMiddleware(transport http.RoundTripper, baseDir string, provider string, logger slog.Logger, clk quartz.Clock) http.RoundTripper {
if baseDir == "" {
return transport
}
return &dumpRoundTripper{
inner: transport,
baseDir: baseDir,
provider: provider,
clk: clk,
logger: logger,
}
}
type dumpRoundTripper struct {
inner http.RoundTripper
baseDir string
provider string
clk quartz.Clock
logger slog.Logger
}
func (rt *dumpRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
dumper := dumper{
dumpPath: passthroughDumpPath(rt.baseDir, rt.provider, req.URL.Path, rt.clk),
logger: rt.logger,
}
if err := dumper.dumpRequest(req); err != nil {
dumper.logger.Named("apidump").Warn(req.Context(), "failed to dump passthrough request", slog.Error(err))
}
resp, err := rt.inner.RoundTrip(req)
if err != nil {
if dumpErr := dumper.dumpError(err); dumpErr != nil {
dumper.logger.Named("apidump").Warn(req.Context(), "failed to dump passthrough request error", slog.Error(dumpErr))
}
return resp, err
}
if err := dumper.dumpResponse(resp); err != nil {
dumper.logger.Named("apidump").Warn(req.Context(), "failed to dump passthrough response", slog.Error(err))
}
return resp, nil
}
// prettyPrintJSON returns indented JSON if body is valid JSON, otherwise returns body as-is.
// Unlike json.MarshalIndent, this preserves the original key order from the input,
// which makes the dumps easier to read and compare with the original requests.
func prettyPrintJSON(body []byte) []byte {
if len(body) == 0 {
return body
}
result := body
if json.Valid(body) {
result = pretty.Pretty(body)
}
return result
}
+500
View File
@@ -0,0 +1,500 @@
package apidump //nolint:testpackage // tests unexported internals
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/quartz"
)
// findDumpFile finds a dump file matching the pattern in the given directory.
func findDumpFile(t *testing.T, dir, suffix string) string {
t.Helper()
pattern := filepath.Join(dir, "*"+suffix)
matches, err := filepath.Glob(pattern)
require.NoError(t, err)
require.Len(t, matches, 1, "expected exactly one %s file in %s", suffix, dir)
return matches[0]
}
func TestBridgedMiddleware_RedactsSensitiveRequestHeaders(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
clk := quartz.NewMock(t)
interceptionID := uuid.New()
middleware := NewBridgeMiddleware(tmpDir, "openai", "gpt-4", interceptionID, logger, clk)
require.NotNil(t, middleware)
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "https://api.openai.com/v1/chat/completions", bytes.NewReader([]byte(`{"test": true}`)))
require.NoError(t, err)
// Add sensitive headers that should be redacted
req.Header.Set("Authorization", "Bearer sk-secret-key-12345")
req.Header.Set("X-Api-Key", "secret-api-key-value")
req.Header.Set("Cookie", "session=abc123")
// Add non-sensitive headers that should be kept as-is
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "test-client")
// Call middleware with a mock next function
resp, err := middleware(req, func(r *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Status: "200 OK",
Proto: "HTTP/1.1",
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(bytes.NewReader([]byte(`{"ok": true}`))),
}, nil
})
require.NoError(t, err)
defer resp.Body.Close()
// Read the request dump file
modelDir := filepath.Join(tmpDir, "openai", "gpt-4")
reqDumpPath := findDumpFile(t, modelDir, SuffixRequest)
reqContent, err := os.ReadFile(reqDumpPath)
require.NoError(t, err)
content := string(reqContent)
// Verify sensitive headers ARE present but redacted
require.Contains(t, content, "Authorization: Bear...2345")
require.Contains(t, content, "X-Api-Key: secr...alue")
require.Contains(t, content, "Cookie: se...23") // "session=abc123" is 14 chars, so first 2 + last 2
// Verify the full secret values are NOT present
require.NotContains(t, content, "sk-secret-key-12345")
require.NotContains(t, content, "secret-api-key-value")
// Verify non-sensitive headers ARE present in full
require.Contains(t, content, "Content-Type: application/json")
require.Contains(t, content, "User-Agent: test-client")
}
func TestBridgedMiddleware_RedactsSensitiveResponseHeaders(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
clk := quartz.NewMock(t)
interceptionID := uuid.New()
middleware := NewBridgeMiddleware(tmpDir, "openai", "gpt-4", interceptionID, logger, clk)
require.NotNil(t, middleware)
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "https://api.openai.com/v1/chat/completions", bytes.NewReader([]byte(`{}`)))
require.NoError(t, err)
// Call middleware with a response containing sensitive headers
resp, err := middleware(req, func(r *http.Request) (*http.Response, error) {
resp := &http.Response{
StatusCode: http.StatusOK,
Status: "200 OK",
Proto: "HTTP/1.1",
Header: make(http.Header),
Body: io.NopCloser(bytes.NewReader([]byte(`{"ok": true}`))),
}
// Add sensitive response headers
resp.Header.Set("Set-Cookie", "session=secret123; HttpOnly; Secure")
resp.Header.Set("WWW-Authenticate", "Bearer realm=\"api\"")
// Add non-sensitive headers
resp.Header.Set("Content-Type", "application/json")
resp.Header.Set("X-Request-Id", "req-123")
return resp, nil
})
require.NoError(t, err)
// Must read and close response body to trigger the streaming dump
_, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
// Read the response dump file
modelDir := filepath.Join(tmpDir, "openai", "gpt-4")
respDumpPath := findDumpFile(t, modelDir, SuffixResponse)
respContent, err := os.ReadFile(respDumpPath)
require.NoError(t, err)
content := string(respContent)
// Verify sensitive headers are present but redacted
require.Contains(t, content, "Set-Cookie: sess...cure")
// Note: Go canonicalizes WWW-Authenticate to Www-Authenticate
// "Bearer realm=\"api\"" = 18 chars, first 2 = "Be", last 2 = "i\""
require.Contains(t, content, "Www-Authenticate: Be...i\"")
// Verify full secret values are NOT present
require.NotContains(t, content, "secret123")
require.NotContains(t, content, "realm=\"api\"")
// Verify non-sensitive headers ARE present in full
require.Contains(t, content, "Content-Type: application/json")
require.Contains(t, content, "X-Request-Id: req-123")
}
func TestBridgedMiddleware_WritesErrorFile_WhenNextFails(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
clk := quartz.NewMock(t)
interceptionID := uuid.New()
middleware := NewBridgeMiddleware(tmpDir, "openai", "gpt-4", interceptionID, logger, clk)
require.NotNil(t, middleware)
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "https://api.openai.com/v1/chat/completions", bytes.NewReader([]byte(`{}`)))
require.NoError(t, err)
upstreamErr := io.ErrUnexpectedEOF
resp, err := middleware(req, func(_ *http.Request) (*http.Response, error) { //nolint:bodyclose // resp is nil on error
return nil, upstreamErr
})
require.ErrorIs(t, err, upstreamErr)
require.Nil(t, resp)
modelDir := filepath.Join(tmpDir, "openai", "gpt-4")
errDumpPath := findDumpFile(t, modelDir, SuffixError)
content, readErr := os.ReadFile(errDumpPath)
require.NoError(t, readErr)
require.Contains(t, string(content), upstreamErr.Error())
}
func TestBridgedMiddleware_EmptyBaseDir_ReturnsNil(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
middleware := NewBridgeMiddleware("", "openai", "gpt-4", uuid.New(), logger, quartz.NewMock(t))
require.Nil(t, middleware)
}
func TestBridgedMiddleware_PreservesRequestBody(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
clk := quartz.NewMock(t)
interceptionID := uuid.New()
middleware := NewBridgeMiddleware(tmpDir, "openai", "gpt-4", interceptionID, logger, clk)
require.NotNil(t, middleware)
originalBody := `{"messages": [{"role": "user", "content": "hello"}]}`
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "https://api.openai.com/v1/chat/completions", bytes.NewReader([]byte(originalBody)))
require.NoError(t, err)
var capturedBody []byte
resp2, err := middleware(req, func(r *http.Request) (*http.Response, error) {
// Read the body in the next handler to verify it's still available
capturedBody, _ = io.ReadAll(r.Body)
return &http.Response{
StatusCode: http.StatusOK,
Status: "200 OK",
Proto: "HTTP/1.1",
Header: http.Header{},
Body: io.NopCloser(bytes.NewReader([]byte(`{}`))),
}, nil
})
require.NoError(t, err)
defer resp2.Body.Close()
// Verify the body was preserved for the next handler
require.Equal(t, originalBody, string(capturedBody))
}
func TestBridgedMiddleware_ModelWithSlash(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
clk := quartz.NewMock(t)
interceptionID := uuid.New()
// Model with slash should have it replaced with dash
middleware := NewBridgeMiddleware(tmpDir, "google", "gemini/1.5-pro", interceptionID, logger, clk)
require.NotNil(t, middleware)
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "https://api.google.com/v1/chat", bytes.NewReader([]byte(`{}`)))
require.NoError(t, err)
resp3, err := middleware(req, func(r *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Status: "200 OK",
Proto: "HTTP/1.1",
Header: http.Header{},
Body: io.NopCloser(bytes.NewReader([]byte(`{}`))),
}, nil
})
require.NoError(t, err)
defer resp3.Body.Close()
// Verify files are created with sanitized model name
modelDir := filepath.Join(tmpDir, "google", "gemini-1.5-pro")
reqDumpPath := findDumpFile(t, modelDir, SuffixRequest)
_, err = os.Stat(reqDumpPath)
require.NoError(t, err)
}
func TestPrettyPrintJSON(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input []byte
expected string
}{
{
name: "empty",
input: []byte{},
expected: "",
},
{
name: "valid JSON",
input: []byte(`{"key":"value"}`),
expected: "{\n \"key\": \"value\"\n}\n",
},
{
name: "invalid JSON returns as-is",
input: []byte("not json"),
expected: "not json",
},
// see: https://github.com/tidwall/pretty/blob/9090695766b652478676cc3e55bc3187056b1ff0/pretty.go#L117
// for input starting with "t" it would change it to "true", eg. "t_rest_of_the_string_is_discarded" -> "true"
// similar for inputs startrting with "f" and "n"
{
name: "invalid JSON edge case t",
input: []byte("test"),
expected: "test",
},
{
name: "invalid JSON edge case f",
input: []byte("f"),
expected: "f",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result := prettyPrintJSON(tc.input)
require.Equal(t, tc.expected, string(result))
})
}
}
func TestBridgedMiddleware_AllSensitiveRequestHeaders(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
clk := quartz.NewMock(t)
interceptionID := uuid.New()
middleware := NewBridgeMiddleware(tmpDir, "openai", "gpt-4", interceptionID, logger, clk)
require.NotNil(t, middleware)
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "https://api.openai.com/v1/chat/completions", bytes.NewReader([]byte(`{}`)))
require.NoError(t, err)
// Set all sensitive headers
req.Header.Set("Authorization", "Bearer sk-secret-key")
req.Header.Set("X-Api-Key", "secret-api-key")
req.Header.Set("Api-Key", "another-secret")
req.Header.Set("X-Auth-Token", "auth-token-val")
req.Header.Set("Cookie", "session=abc123def")
req.Header.Set("Proxy-Authorization", "Basic proxy-creds")
req.Header.Set("X-Amz-Security-Token", "aws-security-token")
resp4, err := middleware(req, func(r *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Status: "200 OK",
Proto: "HTTP/1.1",
Header: http.Header{},
Body: io.NopCloser(bytes.NewReader([]byte(`{}`))),
}, nil
})
require.NoError(t, err)
defer resp4.Body.Close()
modelDir := filepath.Join(tmpDir, "openai", "gpt-4")
reqDumpPath := findDumpFile(t, modelDir, SuffixRequest)
reqContent, err := os.ReadFile(reqDumpPath)
require.NoError(t, err)
content := string(reqContent)
// Verify none of the full secret values are present
require.NotContains(t, content, "sk-secret-key")
require.NotContains(t, content, "secret-api-key")
require.NotContains(t, content, "another-secret")
require.NotContains(t, content, "auth-token-val")
require.NotContains(t, content, "abc123def")
require.NotContains(t, content, "proxy-creds")
require.NotContains(t, content, "aws-security-token")
require.NotContains(t, content, "google-api-key")
// But headers themselves are present (redacted)
require.Contains(t, content, "Authorization:")
require.Contains(t, content, "X-Api-Key:")
require.Contains(t, content, "Api-Key:")
require.Contains(t, content, "X-Auth-Token:")
require.Contains(t, content, "Cookie:")
require.Contains(t, content, "Proxy-Authorization:")
require.Contains(t, content, "X-Amz-Security-Token:")
}
func TestPassthroughMiddleware(t *testing.T) {
t.Parallel()
t.Run("empty_base_dir_returns_original_transport", func(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
inner := http.DefaultTransport
rt := NewPassthroughMiddleware(inner, "", "openai", logger, quartz.NewMock(t))
require.Equal(t, inner, rt)
})
t.Run("returns_error_from_inner_round_trip", func(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
clk := quartz.NewMock(t)
innerErr := io.ErrUnexpectedEOF
inner := &mockRoundTripper{
roundTrip: func(_ *http.Request) (*http.Response, error) {
return nil, innerErr
},
}
rt := NewPassthroughMiddleware(inner, tmpDir, "openai", logger, clk)
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "https://api.openai.com/v1/models", nil)
require.NoError(t, err)
resp, err := rt.RoundTrip(req) //nolint:bodyclose // resp is nil on error
require.ErrorIs(t, err, innerErr)
require.Nil(t, resp)
passthroughDir := filepath.Join(tmpDir, "openai", "passthrough")
errDumpPath := findDumpFile(t, passthroughDir, SuffixError)
content, readErr := os.ReadFile(errDumpPath)
require.NoError(t, readErr)
require.Contains(t, string(content), innerErr.Error())
})
t.Run("dumps_request_and_response", func(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
clk := quartz.NewMock(t)
req1Body := `first request`
req2Body := `{"request": 2}`
req2BodyPretty := "{\n \"request\": 2\n}\n"
callCount := 0
inner := &mockRoundTripper{
roundTrip: func(req *http.Request) (*http.Response, error) {
// Verify body is still readable after dump
body, err := io.ReadAll(req.Body)
require.NoError(t, err)
callCount++
if callCount == 1 {
require.Equal(t, req1Body, string(body))
} else {
require.Equal(t, req2Body, string(body))
}
return &http.Response{
StatusCode: http.StatusOK,
Status: "200 OK",
Proto: "HTTP/1.1",
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(bytes.NewReader([]byte(fmt.Sprintf(`{"call": %d}"`, callCount)))),
}, nil
},
}
rt := NewPassthroughMiddleware(inner, tmpDir, "openai", logger, clk)
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "/v1/models", bytes.NewReader([]byte(req1Body)))
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer sk-secret-key-12345")
resp, err := rt.RoundTrip(req)
require.NoError(t, err)
_, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
// Second request should create new req/resp files
req2, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "/v1/conversations", bytes.NewReader([]byte(req2Body)))
require.NoError(t, err)
resp2, err := rt.RoundTrip(req2)
require.NoError(t, err)
_, err = io.ReadAll(resp2.Body)
require.NoError(t, err)
require.NoError(t, resp2.Body.Close())
// Validate request files contents
passthroughDir := filepath.Join(tmpDir, "openai", "passthrough")
req1Dump := readDumpFileContent(t, filepath.Join(passthroughDir, "*-v1-models-*"+SuffixRequest))
req2Dump := readDumpFileContent(t, filepath.Join(passthroughDir, "*-v1-conversations-*"+SuffixRequest))
require.Contains(t, req1Dump, req1Body+"\n")
require.Contains(t, req2Dump, req2BodyPretty)
// Sensitive header should be redacted
require.NotContains(t, req1Dump, "sk-secret-key-12345")
require.NotContains(t, req2Dump, "sk-secret-key-12345")
require.Contains(t, req1Dump, "Authorization:")
require.NotContains(t, req2Dump, "Authorization:")
// Validate response files contents
resp1Dump := readDumpFileContent(t, filepath.Join(passthroughDir, "*-v1-models-*"+SuffixResponse))
resp2Dump := readDumpFileContent(t, filepath.Join(passthroughDir, "*-v1-conversations-*"+SuffixResponse))
require.Contains(t, resp1Dump, "200 OK")
require.Contains(t, resp1Dump, `{"call": 1}"`)
require.Contains(t, resp2Dump, "200 OK")
require.Contains(t, resp2Dump, `{"call": 2}"`)
})
}
type mockRoundTripper struct {
roundTrip func(*http.Request) (*http.Response, error)
}
func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return m.roundTrip(req)
}
// readDumpFileContent reads the content of the dump file matching the pattern.
// Expects exactly one file to match the pattern.
func readDumpFileContent(t *testing.T, pattern string) string {
t.Helper()
matches, err := filepath.Glob(pattern)
require.NoError(t, err)
require.Len(t, matches, 1, "expected exactly one match got: %v %s", len(matches), strings.Join(matches, ", "), pattern)
reqContent, readErr := os.ReadFile(matches[0])
require.NoError(t, readErr)
return string(reqContent)
}
+20
View File
@@ -0,0 +1,20 @@
package apidump
// sensitiveRequestHeaders are headers that should be redacted from request dumps.
var sensitiveRequestHeaders = map[string]struct{}{
"Authorization": {},
"X-Api-Key": {},
"Api-Key": {},
"X-Auth-Token": {},
"Cookie": {},
"Proxy-Authorization": {},
"X-Amz-Security-Token": {},
}
// sensitiveResponseHeaders are headers that should be redacted from response dumps.
// Note: header names use Go's canonical form (http.CanonicalHeaderKey).
var sensitiveResponseHeaders = map[string]struct{}{
"Set-Cookie": {},
"Www-Authenticate": {},
"Proxy-Authenticate": {},
}
+114
View File
@@ -0,0 +1,114 @@
package apidump //nolint:testpackage // tests unexported internals
import (
"bytes"
"net/http"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3"
"github.com/coder/quartz"
)
func TestSensitiveHeaderLists(t *testing.T) {
t.Parallel()
// Verify all expected sensitive request headers are in the list
expectedRequestHeaders := []string{
"Authorization",
"X-Api-Key",
"Api-Key",
"X-Auth-Token",
"Cookie",
"Proxy-Authorization",
"X-Amz-Security-Token",
}
for _, h := range expectedRequestHeaders {
_, ok := sensitiveRequestHeaders[h]
require.True(t, ok, "expected %q to be in sensitiveRequestHeaders", h)
}
// Verify all expected sensitive response headers are in the list
// Note: header names use Go's canonical form (http.CanonicalHeaderKey)
expectedResponseHeaders := []string{
"Set-Cookie",
"Www-Authenticate",
"Proxy-Authenticate",
}
for _, h := range expectedResponseHeaders {
_, ok := sensitiveResponseHeaders[h]
require.True(t, ok, "expected %q to be in sensitiveResponseHeaders", h)
}
}
func TestWriteRedactedHeaders(t *testing.T) {
t.Parallel()
d := &dumper{
dumpPath: interceptDumpPath("/tmp", "test", "test", uuid.New(), quartz.NewMock(t)),
logger: slog.Make(),
}
tests := []struct {
name string
headers http.Header
sensitive map[string]struct{}
overrides map[string]string
expected string
}{
{
name: "empty headers",
headers: http.Header{},
expected: "",
},
{
name: "single header",
headers: http.Header{"Content-Type": {"application/json"}},
expected: "Content-Type: application/json\r\n",
},
{
name: "sorted alphabetically",
headers: http.Header{
"Zebra": {"last"},
"Alpha": {"first"},
},
expected: "Alpha: first\r\nZebra: last\r\n",
},
{
name: "override applied",
headers: http.Header{"Content-Length": {"100"}},
overrides: map[string]string{"Content-Length": "200"},
expected: "Content-Length: 200\r\n",
},
{
name: "sensitive header redacted",
headers: http.Header{"Set-Cookie": {"session=abcdefghij"}},
sensitive: sensitiveResponseHeaders,
expected: "Set-Cookie: se...ij\r\n",
},
{
name: "multi-value header",
headers: http.Header{
"Accept": {"text/html", "application/json"},
},
expected: "Accept: text/html\r\nAccept: application/json\r\n",
},
{
name: "override for non-existent header",
headers: http.Header{},
overrides: map[string]string{"Host": "example.com"},
expected: "Host: example.com\r\n",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
d.writeRedactedHeaders(&buf, tc.headers, tc.sensitive, tc.overrides)
require.Equal(t, tc.expected, buf.String())
})
}
}
+73
View File
@@ -0,0 +1,73 @@
package apidump
import (
"io"
"os"
"path/filepath"
"sync"
"golang.org/x/xerrors"
)
// streamingBodyDumper wraps an io.ReadCloser and writes all data to a dump file
// as it's read, preserving streaming behavior.
type streamingBodyDumper struct {
body io.ReadCloser
dumpPath string
headerData []byte
logger func(err error)
once sync.Once
file *os.File
initErr error
}
func (s *streamingBodyDumper) init() {
s.once.Do(func() {
if err := os.MkdirAll(filepath.Dir(s.dumpPath), 0o755); err != nil {
s.initErr = xerrors.Errorf("create dump dir: %w", err)
return
}
f, err := os.Create(s.dumpPath)
if err != nil {
s.initErr = xerrors.Errorf("create dump file: %w", err)
return
}
s.file = f
// Write headers first.
if _, err := s.file.Write(s.headerData); err != nil {
s.initErr = xerrors.Errorf("write headers: %w", err)
_ = s.file.Close() // best-effort cleanup on header write failure
s.file = nil
}
})
}
func (s *streamingBodyDumper) Read(p []byte) (int, error) {
n, err := s.body.Read(p)
if n > 0 {
s.init()
if s.initErr != nil && s.logger != nil {
s.logger(s.initErr)
}
if s.file != nil {
// Write raw bytes as they stream through.
_, _ = s.file.Write(p[:n])
}
}
return n, err
}
func (s *streamingBodyDumper) Close() error {
// Ensure init() has completed to avoid racing with Read().
s.init()
var closeErr error
if s.file != nil {
closeErr = s.file.Close()
}
bodyErr := s.body.Close()
if bodyErr != nil {
return bodyErr
}
return closeErr
}
@@ -0,0 +1,129 @@
package apidump //nolint:testpackage // shares test helpers with apidump_test.go
import (
"bytes"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/quartz"
)
func TestMiddleware_StreamingResponse(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
clk := quartz.NewMock(t)
interceptionID := uuid.New()
middleware := NewBridgeMiddleware(tmpDir, "openai", "gpt-4", interceptionID, logger, clk)
require.NotNil(t, middleware)
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "https://api.openai.com/v1/chat/completions", bytes.NewReader([]byte(`{}`)))
require.NoError(t, err)
// Simulate a streaming response with multiple chunks
chunks := []string{
"data: {\"chunk\": 1}\n\n",
"data: {\"chunk\": 2}\n\n",
"data: {\"chunk\": 3}\n\n",
"data: [DONE]\n\n",
}
// Create a pipe to simulate streaming
pr, pw := io.Pipe()
go func() {
defer pw.Close() //nolint:revive // error handled via pipe read side
for _, chunk := range chunks {
if _, err := pw.Write([]byte(chunk)); err != nil {
return
}
}
}()
resp, err := middleware(req, func(r *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Status: "200 OK",
Proto: "HTTP/1.1",
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
Body: pr,
}, nil
})
require.NoError(t, err)
// Read response in small chunks to simulate streaming consumption
var receivedData bytes.Buffer
buf := make([]byte, 16)
for {
n, err := resp.Body.Read(buf)
if n > 0 {
_, _ = receivedData.Write(buf[:n]) // bytes.Buffer.Write never fails
}
if err == io.EOF {
break
}
require.NoError(t, err)
}
require.NoError(t, resp.Body.Close())
// Verify we received all the data
expectedData := strings.Join(chunks, "")
require.Equal(t, expectedData, receivedData.String())
// Verify the dump file was created and contains all the streamed data
modelDir := filepath.Join(tmpDir, "openai", "gpt-4")
respDumpPath := findDumpFile(t, modelDir, SuffixResponse)
respContent, err := os.ReadFile(respDumpPath)
require.NoError(t, err)
content := string(respContent)
require.Contains(t, content, "HTTP/1.1 200 OK")
require.Contains(t, content, "Content-Type: text/event-stream")
// All chunks should be in the dump
for _, chunk := range chunks {
require.Contains(t, content, chunk)
}
}
func TestMiddleware_PreservesResponseBody(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
clk := quartz.NewMock(t)
interceptionID := uuid.New()
middleware := NewBridgeMiddleware(tmpDir, "openai", "gpt-4", interceptionID, logger, clk)
require.NotNil(t, middleware)
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "https://api.openai.com/v1/chat/completions", bytes.NewReader([]byte(`{}`)))
require.NoError(t, err)
originalRespBody := `{"choices": [{"message": {"content": "hi"}}]}`
resp, err := middleware(req, func(r *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Status: "200 OK",
Proto: "HTTP/1.1",
Header: http.Header{},
Body: io.NopCloser(bytes.NewReader([]byte(originalRespBody))),
}, nil
})
require.NoError(t, err)
defer resp.Body.Close()
// Verify the response body is still readable after middleware
capturedBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, originalRespBody, string(capturedBody))
}
+272
View File
@@ -0,0 +1,272 @@
package chatcompletions
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"github.com/google/uuid"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/shared"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/config"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/intercept"
"github.com/coder/coder/v2/aibridge/intercept/apidump"
"github.com/coder/coder/v2/aibridge/mcp"
"github.com/coder/coder/v2/aibridge/recorder"
"github.com/coder/coder/v2/aibridge/tracing"
"github.com/coder/quartz"
)
type interceptionBase struct {
id uuid.UUID
providerName string
req *ChatCompletionNewParamsWrapper
cfg config.OpenAI
// clientHeaders are the original HTTP headers from the client request.
clientHeaders http.Header
authHeaderName string
logger slog.Logger
tracer trace.Tracer
recorder recorder.Recorder
mcpProxy mcp.ServerProxier
credential intercept.CredentialInfo
}
func (i *interceptionBase) newCompletionsService() openai.ChatCompletionService {
opts := []option.RequestOption{option.WithAPIKey(i.cfg.Key), option.WithBaseURL(i.cfg.BaseURL)}
if i.cfg.MaxRetries != nil {
opts = append(opts, option.WithMaxRetries(*i.cfg.MaxRetries))
}
// Add extra headers if configured.
// Some providers require additional headers that are not added by the SDK.
// TODO(ssncferreira): remove as part of https://github.com/coder/aibridge/issues/192
for key, value := range i.cfg.ExtraHeaders {
opts = append(opts, option.WithHeader(key, value))
}
// Forward client headers to upstream. This middleware runs after the SDK
// has built the request, and replaces the outgoing headers with the sanitized
// client headers plus provider auth.
if i.clientHeaders != nil {
opts = append(opts, option.WithMiddleware(func(req *http.Request, next option.MiddlewareNext) (*http.Response, error) {
req.Header = intercept.BuildUpstreamHeaders(req.Header, i.clientHeaders, i.authHeaderName)
return next(req)
}))
}
// Add API dump middleware if configured
if mw := apidump.NewBridgeMiddleware(i.cfg.APIDumpDir, i.providerName, i.Model(), i.id, i.logger, quartz.NewReal()); mw != nil {
opts = append(opts, option.WithMiddleware(mw))
}
return openai.NewChatCompletionService(opts...)
}
func (i *interceptionBase) ID() uuid.UUID {
return i.id
}
func (i *interceptionBase) Credential() intercept.CredentialInfo {
return i.credential
}
func (i *interceptionBase) Setup(logger slog.Logger, rec recorder.Recorder, mcpProxy mcp.ServerProxier) {
i.logger = logger
i.recorder = rec
i.mcpProxy = mcpProxy
}
func (i *interceptionBase) CorrelatingToolCallID() *string {
if len(i.req.Messages) == 0 {
return nil
}
// The tool result should be the last input message.
msg := i.req.Messages[len(i.req.Messages)-1]
if msg.OfTool == nil {
return nil
}
return &msg.OfTool.ToolCallID
}
func (i *interceptionBase) baseTraceAttributes(r *http.Request, streaming bool) []attribute.KeyValue {
return []attribute.KeyValue{
attribute.String(tracing.RequestPath, r.URL.Path),
attribute.String(tracing.InterceptionID, i.id.String()),
attribute.String(tracing.InitiatorID, aibcontext.ActorIDFromContext(r.Context())),
attribute.String(tracing.Provider, i.providerName),
attribute.String(tracing.Model, i.Model()),
attribute.Bool(tracing.Streaming, streaming),
}
}
func (i *interceptionBase) Model() string {
if i.req == nil {
return "coder-aibridge-unknown"
}
return i.req.Model
}
func (*interceptionBase) newErrorResponse(err error) map[string]any {
return map[string]any{
"error": true,
"message": err.Error(),
}
}
func (i *interceptionBase) injectTools() {
if i.req == nil || i.mcpProxy == nil || !i.hasInjectableTools() {
return
}
// Disable parallel tool calls when injectable tools are present to simplify the inner agentic loop.
i.req.ParallelToolCalls = openai.Bool(false)
// Inject tools.
for _, tool := range i.mcpProxy.ListTools() {
fn := openai.ChatCompletionToolUnionParam{
OfFunction: &openai.ChatCompletionFunctionToolParam{
Function: openai.FunctionDefinitionParam{
Name: tool.ID,
Strict: openai.Bool(false), // TODO: configurable.
Description: openai.String(tool.Description),
Parameters: openai.FunctionParameters{
"type": "object",
"properties": tool.Params,
// "additionalProperties": false, // Only relevant when strict=true.
},
},
},
}
// Otherwise the request fails with "None is not of type 'array'" if a nil slice is given.
if len(tool.Required) > 0 {
// Must list ALL properties when strict=true.
fn.OfFunction.Function.Parameters["required"] = tool.Required
}
i.req.Tools = append(i.req.Tools, fn)
}
}
func (i *interceptionBase) unmarshalArgs(in string) (args recorder.ToolArgs) {
if len(strings.TrimSpace(in)) == 0 {
return args // An empty string will fail JSON unmarshaling.
}
if err := json.Unmarshal([]byte(in), &args); err != nil {
i.logger.Warn(context.Background(), "failed to unmarshal tool args", slog.Error(err))
}
return args
}
// writeUpstreamError marshals and writes a given error.
func (i *interceptionBase) writeUpstreamError(w http.ResponseWriter, oaiErr *responseError) {
if oaiErr == nil {
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(oaiErr.StatusCode)
out, err := json.Marshal(oaiErr)
if err != nil {
i.logger.Warn(context.Background(), "failed to marshal upstream error", slog.Error(err), slog.F("error_payload", fmt.Sprintf("%+v", oaiErr)))
// Response has to match expected format.
_, _ = w.Write([]byte(`{
"error": {
"type": "error",
"message":"error marshaling upstream error",
"code": "server_error"
},
}`))
} else {
_, _ = w.Write(out)
}
}
func (i *interceptionBase) hasInjectableTools() bool {
return i.mcpProxy != nil && len(i.mcpProxy.ListTools()) > 0
}
func sumUsage(ref, in openai.CompletionUsage) openai.CompletionUsage {
return openai.CompletionUsage{
CompletionTokens: ref.CompletionTokens + in.CompletionTokens,
PromptTokens: ref.PromptTokens + in.PromptTokens,
TotalTokens: ref.TotalTokens + in.TotalTokens,
CompletionTokensDetails: openai.CompletionUsageCompletionTokensDetails{
AcceptedPredictionTokens: ref.CompletionTokensDetails.AcceptedPredictionTokens + in.CompletionTokensDetails.AcceptedPredictionTokens,
AudioTokens: ref.CompletionTokensDetails.AudioTokens + in.CompletionTokensDetails.AudioTokens,
ReasoningTokens: ref.CompletionTokensDetails.ReasoningTokens + in.CompletionTokensDetails.ReasoningTokens,
RejectedPredictionTokens: ref.CompletionTokensDetails.RejectedPredictionTokens + in.CompletionTokensDetails.RejectedPredictionTokens,
},
PromptTokensDetails: openai.CompletionUsagePromptTokensDetails{
AudioTokens: ref.PromptTokensDetails.AudioTokens + in.PromptTokensDetails.AudioTokens,
CachedTokens: ref.PromptTokensDetails.CachedTokens + in.PromptTokensDetails.CachedTokens,
},
}
}
// calculateActualInputTokenUsage accounts for cached tokens which are included in [openai.CompletionUsage].PromptTokens.
func calculateActualInputTokenUsage(in openai.CompletionUsage) int64 {
// Input *includes* the cached tokens, so we subtract them here to reflect actual input token usage.
// The original value can be reconstructed by adding CachedTokens back to Input.
// See https://platform.openai.com/docs/api-reference/usage/completions_object#usage/completions_object-input_tokens.
return in.PromptTokens /* The aggregated number of text input tokens used, including cached tokens. */ -
in.PromptTokensDetails.CachedTokens /* The aggregated number of text input tokens that has been cached from previous requests. */
}
func getErrorResponse(err error) *responseError {
var apiErr *openai.Error
if !errors.As(err, &apiErr) {
return nil
}
return &responseError{
ErrorObject: &shared.ErrorObject{
Code: apiErr.Code,
Message: apiErr.Message,
Type: apiErr.Type,
},
StatusCode: apiErr.StatusCode,
}
}
var _ error = &responseError{}
type responseError struct {
ErrorObject *shared.ErrorObject `json:"error"`
StatusCode int `json:"-"`
}
func newErrorResponse(msg error) *responseError {
return &responseError{
ErrorObject: &shared.ErrorObject{
Code: "error",
Message: msg.Error(),
Type: "error",
},
}
}
func (a *responseError) Error() string {
if a.ErrorObject == nil {
return ""
}
return a.ErrorObject.Message
}
@@ -0,0 +1,77 @@
package chatcompletions //nolint:testpackage // tests unexported internals
import (
"testing"
"github.com/openai/openai-go/v3"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/aibridge/utils"
)
func TestScanForCorrelatingToolCallID(t *testing.T) {
t.Parallel()
tests := []struct {
name string
messages []openai.ChatCompletionMessageParamUnion
expected *string
}{
{
name: "no messages",
messages: nil,
expected: nil,
},
{
name: "no tool messages",
messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("hello"),
openai.AssistantMessage("hi there"),
},
expected: nil,
},
{
name: "single tool message",
messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("hello"),
openai.ToolMessage("result", "call_abc"),
},
expected: utils.PtrTo("call_abc"),
},
{
name: "multiple tool messages returns last",
messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("hello"),
openai.ToolMessage("first result", "call_first"),
openai.AssistantMessage("thinking"),
openai.ToolMessage("second result", "call_second"),
},
expected: utils.PtrTo("call_second"),
},
{
name: "last message is not a tool message",
messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("hello"),
openai.ToolMessage("first result", "call_first"),
openai.AssistantMessage("thinking"),
},
expected: nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
base := &interceptionBase{
req: &ChatCompletionNewParamsWrapper{
ChatCompletionNewParams: openai.ChatCompletionNewParams{
Messages: tc.messages,
},
},
}
require.Equal(t, tc.expected, base.CorrelatingToolCallID())
})
}
}
@@ -0,0 +1,266 @@
package chatcompletions
import (
"context"
"encoding/json"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/config"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/intercept"
"github.com/coder/coder/v2/aibridge/intercept/eventstream"
"github.com/coder/coder/v2/aibridge/mcp"
"github.com/coder/coder/v2/aibridge/recorder"
"github.com/coder/coder/v2/aibridge/tracing"
)
type BlockingInterception struct {
interceptionBase
}
func NewBlockingInterceptor(
id uuid.UUID,
req *ChatCompletionNewParamsWrapper,
providerName string,
cfg config.OpenAI,
clientHeaders http.Header,
authHeaderName string,
tracer trace.Tracer,
cred intercept.CredentialInfo,
) *BlockingInterception {
return &BlockingInterception{interceptionBase: interceptionBase{
id: id,
providerName: providerName,
req: req,
cfg: cfg,
clientHeaders: clientHeaders,
authHeaderName: authHeaderName,
tracer: tracer,
credential: cred,
}}
}
func (i *BlockingInterception) Setup(logger slog.Logger, rec recorder.Recorder, mcpProxy mcp.ServerProxier) {
i.interceptionBase.Setup(logger.Named("blocking"), rec, mcpProxy)
}
func (*BlockingInterception) Streaming() bool {
return false
}
func (i *BlockingInterception) TraceAttributes(r *http.Request) []attribute.KeyValue {
return i.interceptionBase.baseTraceAttributes(r, false)
}
func (i *BlockingInterception) ProcessRequest(w http.ResponseWriter, r *http.Request) (outErr error) {
if i.req == nil {
return xerrors.New("developer error: req is nil")
}
ctx, span := i.tracer.Start(r.Context(), "Intercept.ProcessRequest", trace.WithAttributes(tracing.InterceptionAttributesFromContext(r.Context())...))
defer tracing.EndSpanErr(span, &outErr)
svc := i.newCompletionsService()
logger := i.logger.With(slog.F("model", i.req.Model))
var (
cumulativeUsage openai.CompletionUsage
completion *openai.ChatCompletion
err error
)
i.injectTools()
prompt, err := i.req.lastUserPrompt()
if err != nil {
logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err))
}
for {
// TODO add outer loop span (https://github.com/coder/aibridge/issues/67)
var opts []option.RequestOption
opts = append(opts, option.WithRequestTimeout(time.Second*600))
// TODO(ssncferreira): inject actor headers directly in the client-header
// middleware instead of using SDK options.
if actor := aibcontext.ActorFromContext(r.Context()); actor != nil && i.cfg.SendActorHeaders {
opts = append(opts, intercept.ActorHeadersAsOpenAIOpts(actor)...)
}
completion, err = i.newChatCompletion(ctx, svc, opts)
if err != nil {
break
}
if prompt != nil {
_ = i.recorder.RecordPromptUsage(ctx, &recorder.PromptUsageRecord{
InterceptionID: i.ID().String(),
MsgID: completion.ID,
Prompt: *prompt,
})
prompt = nil
}
lastUsage := completion.Usage
cumulativeUsage = sumUsage(cumulativeUsage, completion.Usage)
_ = i.recorder.RecordTokenUsage(ctx, &recorder.TokenUsageRecord{
InterceptionID: i.ID().String(),
MsgID: completion.ID,
Input: calculateActualInputTokenUsage(lastUsage),
Output: lastUsage.CompletionTokens,
CacheReadInputTokens: lastUsage.PromptTokensDetails.CachedTokens,
ExtraTokenTypes: map[string]int64{
"prompt_audio": lastUsage.PromptTokensDetails.AudioTokens,
"prompt_cached": lastUsage.PromptTokensDetails.CachedTokens, // TODO: remove from ExtraTokenTypes (https://github.com/coder/aibridge/issues/243)
"completion_accepted_prediction": lastUsage.CompletionTokensDetails.AcceptedPredictionTokens,
"completion_rejected_prediction": lastUsage.CompletionTokensDetails.RejectedPredictionTokens,
"completion_audio": lastUsage.CompletionTokensDetails.AudioTokens,
"completion_reasoning": lastUsage.CompletionTokensDetails.ReasoningTokens,
},
})
// Check if we have tool calls to process.
var pendingToolCalls []openai.ChatCompletionMessageToolCallUnion
if len(completion.Choices) > 0 && completion.Choices[0].Message.ToolCalls != nil {
for _, toolCall := range completion.Choices[0].Message.ToolCalls {
if i.mcpProxy != nil && i.mcpProxy.GetTool(toolCall.Function.Name) != nil {
pendingToolCalls = append(pendingToolCalls, toolCall)
} else {
_ = i.recorder.RecordToolUsage(ctx, &recorder.ToolUsageRecord{
InterceptionID: i.ID().String(),
MsgID: completion.ID,
ToolCallID: toolCall.ID,
Tool: toolCall.Function.Name,
Args: i.unmarshalArgs(toolCall.Function.Arguments),
Injected: false,
})
}
}
}
// If no injected tool calls, we're done.
if len(pendingToolCalls) == 0 {
break
}
appendedPrevMsg := false
for _, tc := range pendingToolCalls {
if i.mcpProxy == nil {
continue
}
tool := i.mcpProxy.GetTool(tc.Function.Name)
if tool == nil {
// Not a known tool, don't do anything.
logger.Warn(ctx, "pending tool call for non-managed tool, skipping", slog.F("tool", tc.Function.Name))
continue
}
// Only do this once.
if !appendedPrevMsg {
// Append the whole message from this stream as context since we'll be sending a new request with the tool results.
i.req.Messages = append(i.req.Messages, completion.Choices[0].Message.ToParam())
appendedPrevMsg = true
}
args := i.unmarshalArgs(tc.Function.Arguments)
res, err := tool.Call(ctx, args, i.tracer)
_ = i.recorder.RecordToolUsage(ctx, &recorder.ToolUsageRecord{
InterceptionID: i.ID().String(),
MsgID: completion.ID,
ToolCallID: tc.ID,
ServerURL: &tool.ServerURL,
Tool: tool.Name,
Args: args,
Injected: true,
InvocationError: err,
})
if err != nil {
// Always provide a tool result even if the tool call failed
errorResponse := map[string]interface{}{
// TODO: interception ID?
"error": true,
"message": err.Error(),
}
errorJSON, _ := json.Marshal(errorResponse)
i.req.Messages = append(i.req.Messages, openai.ToolMessage(string(errorJSON), tc.ID))
continue
}
var out strings.Builder
if err := json.NewEncoder(&out).Encode(res); err != nil {
logger.Warn(ctx, "failed to encode tool response", slog.Error(err))
// Always provide a tool result even if encoding failed
errorResponse := map[string]interface{}{
// TODO: interception ID?
"error": true,
"message": err.Error(),
}
errorJSON, _ := json.Marshal(errorResponse)
i.req.Messages = append(i.req.Messages, openai.ToolMessage(string(errorJSON), tc.ID))
continue
}
i.req.Messages = append(i.req.Messages, openai.ToolMessage(out.String(), tc.ID))
}
}
if err != nil {
if eventstream.IsConnError(err) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return xerrors.Errorf("upstream connection closed: %w", err)
}
if apiErr := getErrorResponse(err); apiErr != nil {
i.writeUpstreamError(w, apiErr)
return xerrors.Errorf("openai API error: %w", err)
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return xerrors.Errorf("chat completion failed: %w", err)
}
if completion == nil {
return nil
}
// Overwrite response identifier since proxy obscures injected tool call invocations.
completion.ID = i.ID().String()
// Update the cumulative usage in the final response.
if completion.Usage.CompletionTokens > 0 {
completion.Usage = cumulativeUsage
}
w.Header().Set("Content-Type", "application/json")
out, err := json.Marshal(completion)
if err != nil {
out, _ = json.Marshal(i.newErrorResponse(xerrors.Errorf("failed to marshal response: %w", err)))
w.WriteHeader(http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusOK)
}
_, _ = w.Write(out)
return nil
}
func (i *BlockingInterception) newChatCompletion(ctx context.Context, svc openai.ChatCompletionService, opts []option.RequestOption) (_ *openai.ChatCompletion, outErr error) {
ctx, span := i.tracer.Start(ctx, "Intercept.ProcessRequest.Upstream", trace.WithAttributes(tracing.InterceptionAttributesFromContext(ctx)...))
defer tracing.EndSpanErr(span, &outErr)
return svc.New(ctx, i.req.ChatCompletionNewParams, opts...)
}
@@ -0,0 +1,73 @@
package chatcompletions
import (
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/packages/param"
"github.com/tidwall/gjson"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/aibridge/utils"
)
// ChatCompletionNewParamsWrapper exists because the "stream" param is not included in openai.ChatCompletionNewParams.
type ChatCompletionNewParamsWrapper struct {
openai.ChatCompletionNewParams `json:""`
Stream bool `json:"stream,omitempty"`
}
func (c ChatCompletionNewParamsWrapper) MarshalJSON() ([]byte, error) {
type shadow ChatCompletionNewParamsWrapper
return param.MarshalWithExtras(c, (*shadow)(&c), map[string]any{
"stream": c.Stream,
})
}
func (c *ChatCompletionNewParamsWrapper) UnmarshalJSON(raw []byte) error {
err := c.ChatCompletionNewParams.UnmarshalJSON(raw)
if err != nil {
return err
}
c.Stream = gjson.GetBytes(raw, "stream").Bool()
if c.Stream {
c.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{
IncludeUsage: openai.Bool(true), // Always include usage when streaming.
}
} else {
c.ChatCompletionNewParams.StreamOptions = openai.ChatCompletionStreamOptionsParam{}
}
return nil
}
func (c *ChatCompletionNewParamsWrapper) lastUserPrompt() (*string, error) {
if c == nil {
return nil, xerrors.New("nil struct")
}
if len(c.Messages) == 0 {
return nil, xerrors.New("no messages")
}
// We only care if the last message was issued by a user.
msg := c.Messages[len(c.Messages)-1]
if msg.OfUser == nil {
return nil, nil //nolint:nilnil // no user prompt found is not an error
}
if msg.OfUser.Content.OfString.String() != "" {
return utils.PtrTo(msg.OfUser.Content.OfString.String()), nil
}
// Walk backwards on "user"-initiated message content. Clients often inject
// content ahead of the actual prompt to provide context to the model,
// so the last item in the slice is most likely the user's prompt.
for i := len(msg.OfUser.Content.OfArrayOfContentParts) - 1; i >= 0; i-- {
// Only text content is supported currently.
if textContent := msg.OfUser.Content.OfArrayOfContentParts[i].OfText; textContent != nil {
return &textContent.Text, nil
}
}
return nil, nil //nolint:nilnil // no text content found is not an error
}
@@ -0,0 +1,174 @@
package chatcompletions //nolint:testpackage // tests unexported internals
import (
"fmt"
"strings"
"testing"
"github.com/openai/openai-go/v3"
"github.com/stretchr/testify/require"
)
func TestOpenAILastUserPrompt(t *testing.T) {
t.Parallel()
tests := []struct {
name string
wrapper *ChatCompletionNewParamsWrapper
expected string
expectError bool
errorMsg string
}{
{
name: "nil struct",
expectError: true,
errorMsg: "nil struct",
},
{
name: "no messages",
wrapper: &ChatCompletionNewParamsWrapper{
ChatCompletionNewParams: openai.ChatCompletionNewParams{
Messages: []openai.ChatCompletionMessageParamUnion{},
},
},
expectError: true,
errorMsg: "no messages",
},
{
name: "last message not from user",
wrapper: &ChatCompletionNewParamsWrapper{
ChatCompletionNewParams: openai.ChatCompletionNewParams{
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("user message"),
openai.AssistantMessage("assistant message"),
},
},
},
},
{
name: "user message with string content",
wrapper: &ChatCompletionNewParamsWrapper{
ChatCompletionNewParams: openai.ChatCompletionNewParams{
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("Hello, world!"),
},
},
},
expected: "Hello, world!",
},
{
name: "user message with empty string",
wrapper: &ChatCompletionNewParamsWrapper{
ChatCompletionNewParams: openai.ChatCompletionNewParams{
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage(""),
},
},
},
},
{
name: "user message with array content - text at end",
wrapper: &ChatCompletionNewParamsWrapper{
ChatCompletionNewParams: openai.ChatCompletionNewParams{
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage([]openai.ChatCompletionContentPartUnionParam{
openai.ImageContentPart(openai.ChatCompletionContentPartImageImageURLParam{
URL: "https://example.com/image.png",
}),
openai.TextContentPart("First text"),
openai.ImageContentPart(openai.ChatCompletionContentPartImageImageURLParam{
URL: "https://example.com/image2.png",
}),
openai.TextContentPart("Last text"),
}),
},
},
},
expected: "Last text",
},
{
name: "user message with array content - no text",
wrapper: &ChatCompletionNewParamsWrapper{
ChatCompletionNewParams: openai.ChatCompletionNewParams{
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage([]openai.ChatCompletionContentPartUnionParam{
openai.ImageContentPart(openai.ChatCompletionContentPartImageImageURLParam{
URL: "https://example.com/image.png",
}),
}),
},
},
},
},
{
name: "user message with empty array",
wrapper: &ChatCompletionNewParamsWrapper{
ChatCompletionNewParams: openai.ChatCompletionNewParams{
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage([]openai.ChatCompletionContentPartUnionParam{}),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result, err := tt.wrapper.lastUserPrompt()
if tt.expectError {
require.Error(t, err)
require.Contains(t, err.Error(), tt.errorMsg)
require.Nil(t, result)
} else {
require.NoError(t, err)
if tt.expected == "" {
require.Nil(t, result)
} else {
require.NotNil(t, result)
require.Equal(t, tt.expected, *result)
}
}
})
}
}
// generatePayload creates a JSON payload with the specified number of messages.
// Messages alternate between user and assistant roles to simulate a conversation.
func generatePayload(messageCount int) []byte {
var messages []string
for i := range messageCount {
role := "user"
if i%2 == 1 {
role = "assistant"
}
// Use realistic message content size
content := fmt.Sprintf("This is message number %d with some realistic content that might appear in a conversation.", i+1)
messages = append(messages, fmt.Sprintf(`{"role": %q, "content": %q}`, role, content))
}
return []byte(fmt.Sprintf(`{
"model": "gpt-4",
"stream": true,
"messages": [%s]
}`, strings.Join(messages, ",")))
}
func BenchmarkChatCompletionNewParamsWrapper_UnmarshalJSON(b *testing.B) {
messageCounts := []int{1, 10, 20, 50}
for _, count := range messageCounts {
payload := generatePayload(count)
b.Run(fmt.Sprintf("messages=%d", count), func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for range b.N {
var wrapper ChatCompletionNewParamsWrapper
_ = wrapper.UnmarshalJSON(payload)
}
})
}
}
@@ -0,0 +1,549 @@
package chatcompletions
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"slices"
"strings"
"time"
"github.com/google/uuid"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/packages/ssestream"
"github.com/tidwall/sjson"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/config"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/intercept"
"github.com/coder/coder/v2/aibridge/intercept/eventstream"
"github.com/coder/coder/v2/aibridge/mcp"
"github.com/coder/coder/v2/aibridge/recorder"
"github.com/coder/coder/v2/aibridge/tracing"
"github.com/coder/quartz"
)
type StreamingInterception struct {
interceptionBase
}
func NewStreamingInterceptor(
id uuid.UUID,
req *ChatCompletionNewParamsWrapper,
providerName string,
cfg config.OpenAI,
clientHeaders http.Header,
authHeaderName string,
tracer trace.Tracer,
cred intercept.CredentialInfo,
) *StreamingInterception {
return &StreamingInterception{interceptionBase: interceptionBase{
id: id,
providerName: providerName,
req: req,
cfg: cfg,
clientHeaders: clientHeaders,
authHeaderName: authHeaderName,
tracer: tracer,
credential: cred,
}}
}
func (i *StreamingInterception) Setup(logger slog.Logger, rec recorder.Recorder, mcpProxy mcp.ServerProxier) {
i.interceptionBase.Setup(logger.Named("streaming"), rec, mcpProxy)
}
func (*StreamingInterception) Streaming() bool {
return true
}
func (i *StreamingInterception) TraceAttributes(r *http.Request) []attribute.KeyValue {
return i.interceptionBase.baseTraceAttributes(r, true)
}
// ProcessRequest handles a request to /v1/chat/completions.
// See https://platform.openai.com/docs/api-reference/chat-streaming/streaming.
//
// It will inject any tools which have been provided by the [mcp.ServerProxier].
//
// When a response from the server includes an event indicating that a tool must be invoked, a conditional
// flow takes place:
//
// a) if the tool is not injected (i.e. defined by the client), relay the event unmodified
// b) if the tool is injected, it will be invoked by the [mcp.ServerProxier] in the remote MCP server, and its
// results relayed to the SERVER. The response from the server will be handled synchronously, and this loop
// can continue until all injected tool invocations are completed and the response is relayed to the client.
func (i *StreamingInterception) ProcessRequest(w http.ResponseWriter, r *http.Request) (outErr error) {
if i.req == nil {
return xerrors.New("developer error: req is nil")
}
ctx, span := i.tracer.Start(r.Context(), "Intercept.ProcessRequest", trace.WithAttributes(tracing.InterceptionAttributesFromContext(r.Context())...))
defer tracing.EndSpanErr(span, &outErr)
// Include token usage.
i.req.StreamOptions.IncludeUsage = openai.Bool(true)
i.injectTools()
// Allow us to interrupt watch via cancel.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
r = r.WithContext(ctx) // Rewire context for SSE cancellation.
svc := i.newCompletionsService()
logger := i.logger.With(slog.F("model", i.req.Model))
streamCtx, streamCancel := context.WithCancelCause(ctx)
defer streamCancel(xerrors.New("deferred"))
// events will either terminate when shutdown after interaction with upstream completes, or when streamCtx is done.
events := eventstream.NewEventStream(streamCtx, logger.Named("sse-sender"), nil, quartz.NewReal())
go events.Start(w, r)
defer func() {
_ = events.Shutdown(streamCtx) // Catch-all in case it doesn't get shutdown after stream completes.
}()
// Force responses to only have one choice.
// It's unnecessary to generate multiple responses, and would complicate our stream processing logic if
// multiple choices were returned.
i.req.N = openai.Int(1)
prompt, err := i.req.lastUserPrompt()
if err != nil {
logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(err))
}
var (
stream *ssestream.Stream[openai.ChatCompletionChunk]
lastErr error
interceptionErr error
)
for {
// TODO add outer loop span (https://github.com/coder/aibridge/issues/67)
var opts []option.RequestOption
// TODO(ssncferreira): inject actor headers directly in the client-header
// middleware instead of using SDK options.
if actor := aibcontext.ActorFromContext(r.Context()); actor != nil && i.cfg.SendActorHeaders {
opts = append(opts, intercept.ActorHeadersAsOpenAIOpts(actor)...)
}
// We take control of request body here and pass it to the SDK as a raw byte slice.
// This is because the SDK's serialization applies hidden request options that result in
// unexpected, breaking behavior. See https://github.com/coder/aibridge/pull/164
body, err := json.Marshal(i.req.ChatCompletionNewParams)
if err != nil {
return xerrors.Errorf("marshal request body: %w", err)
}
opts = append(opts, option.WithRequestBody("application/json", body))
opts = append(opts, option.WithJSONSet("stream", true))
stream = i.newStream(streamCtx, svc, opts)
processor := newStreamProcessor(streamCtx, i.logger.Named("stream-processor"), i.getInjectedToolByName)
var toolCall *openai.FinishedChatCompletionToolCall
for stream.Next() {
chunk := stream.Current()
canRelay := processor.process(chunk)
if toolCall == nil {
toolCall = processor.getToolCall()
}
if !canRelay {
// The chunk must not be sent to the client because it contains an injected tool call.
continue
}
// Marshal and relay chunk to client.
payload, err := i.marshalChunk(&chunk, i.ID(), processor)
if err != nil {
logger.Warn(ctx, "failed to marshal chunk", slog.Error(err), slog.F("chunk", chunk.RawJSON()))
lastErr = xerrors.Errorf("marshal chunk: %w", err)
break
}
if err := events.Send(ctx, payload); err != nil {
logger.Warn(ctx, "failed to relay chunk", slog.Error(err))
lastErr = xerrors.Errorf("relay chunk: %w", err)
break
}
}
if toolCall != nil {
// Builtin tools are not intercepted.
if i.getInjectedToolByName(toolCall.Name) == nil {
_ = i.recorder.RecordToolUsage(streamCtx, &recorder.ToolUsageRecord{
InterceptionID: i.ID().String(),
MsgID: processor.getMsgID(),
ToolCallID: toolCall.ID,
Tool: toolCall.Name,
Args: i.unmarshalArgs(toolCall.Arguments),
Injected: false,
})
toolCall = nil
} else if stream.Err() == nil {
// When the provider responds with only tool calls (no text content),
// no chunks are relayed to the client, so the stream is not yet
// initiated. Initiate it here so the SSE headers are sent and the
// ping ticker is started, preventing client timeout during tool invocation.
// Only initiate if no stream error, if there's an error, we'll return
// an HTTP error response instead of starting an SSE stream.
events.InitiateStream(w)
}
}
if prompt != nil {
_ = i.recorder.RecordPromptUsage(streamCtx, &recorder.PromptUsageRecord{
InterceptionID: i.ID().String(),
MsgID: processor.getMsgID(),
Prompt: *prompt,
})
prompt = nil
}
if lastUsage := processor.getLastUsage(); lastUsage.CompletionTokens > 0 {
// If the usage information is set, track it.
// The API will send usage information when the response terminates, which will happen if a tool call is invoked.
_ = i.recorder.RecordTokenUsage(streamCtx, &recorder.TokenUsageRecord{
InterceptionID: i.ID().String(),
MsgID: processor.getMsgID(),
Input: calculateActualInputTokenUsage(lastUsage),
Output: lastUsage.CompletionTokens,
CacheReadInputTokens: lastUsage.PromptTokensDetails.CachedTokens,
ExtraTokenTypes: map[string]int64{
"prompt_audio": lastUsage.PromptTokensDetails.AudioTokens,
"prompt_cached": lastUsage.PromptTokensDetails.CachedTokens, // TODO: remove from ExtraTokenTypes (https://github.com/coder/aibridge/issues/243)
"completion_accepted_prediction": lastUsage.CompletionTokensDetails.AcceptedPredictionTokens,
"completion_rejected_prediction": lastUsage.CompletionTokensDetails.RejectedPredictionTokens,
"completion_audio": lastUsage.CompletionTokensDetails.AudioTokens,
"completion_reasoning": lastUsage.CompletionTokensDetails.ReasoningTokens,
},
})
}
if !events.IsStreaming() {
// response/downstream Stream has not started yet; write error response and exit.
i.writeUpstreamError(w, getErrorResponse(stream.Err()))
return stream.Err()
}
// Check if the stream encountered any errors.
if streamErr := stream.Err(); streamErr != nil {
if eventstream.IsUnrecoverableError(streamErr) {
logger.Debug(ctx, "stream terminated", slog.Error(streamErr))
// We can't reflect an error back if there's a connection error or the request context was canceled.
} else if oaiErr := getErrorResponse(streamErr); oaiErr != nil {
logger.Warn(ctx, "openai stream error", slog.Error(streamErr))
interceptionErr = oaiErr
} else {
logger.Warn(ctx, "unknown stream error", slog.Error(streamErr))
// Unfortunately, the OpenAI SDK does not support parsing errors received in the stream
// into known types (i.e. [shared.OverloadedError]).
// See https://github.com/openai/openai-go/blob/v2.7.0/packages/ssestream/ssestream.go#L171
// All it does is wrap the payload in an error - which is all we can return, currently.
interceptionErr = newErrorResponse(xerrors.Errorf("unknown stream error: %w", streamErr))
}
} else if lastErr != nil {
// Otherwise check if any logical errors occurred during processing.
logger.Warn(ctx, "stream processing failed", slog.Error(lastErr))
interceptionErr = newErrorResponse(xerrors.Errorf("processing error: %w", lastErr))
}
if interceptionErr != nil {
payload, err := i.marshalErr(interceptionErr)
if err != nil {
logger.Warn(ctx, "failed to marshal error", slog.Error(err), slog.F("error_payload", fmt.Sprintf("%+v", interceptionErr)))
} else if err := events.Send(streamCtx, payload); err != nil {
logger.Warn(ctx, "failed to relay error", slog.Error(err), slog.F("payload", payload))
}
}
// No tool call, nothing more to do.
if toolCall == nil {
break
}
tool := i.getInjectedToolByName(toolCall.Name)
if tool == nil {
// Not a known tool, don't do anything.
logger.Warn(streamCtx, "pending tool call for non-injected tool, this is unexpected", slog.F("tool", toolCall.Name))
break
}
// Invoke the injected tool, and use the tool result to make a subsequent request to the upstream.
// Append the completion from this stream as context.
// Some providers may return tool calls with non-zero starting indices,
// resulting in nil entries in the array that must be removed.
completion := processor.getLastCompletion()
if completion != nil {
compactToolCalls(completion)
i.req.Messages = append(i.req.Messages, completion.ToParam())
}
id := toolCall.ID
args := i.unmarshalArgs(toolCall.Arguments)
toolRes, toolErr := tool.Call(streamCtx, args, i.tracer)
_ = i.recorder.RecordToolUsage(streamCtx, &recorder.ToolUsageRecord{
InterceptionID: i.ID().String(),
MsgID: processor.getMsgID(),
ToolCallID: id,
ServerURL: &tool.ServerURL,
Tool: tool.Name,
Args: args,
Injected: true,
InvocationError: toolErr,
})
// Reset.
toolCall = nil
if toolErr != nil {
// Always provide a tool_result even if the tool call failed.
errorJSON, _ := json.Marshal(i.newErrorResponse(toolErr))
i.req.Messages = append(i.req.Messages, openai.ToolMessage(string(errorJSON), id))
continue
}
var out strings.Builder
if err := json.NewEncoder(&out).Encode(toolRes); err != nil {
logger.Warn(ctx, "failed to encode tool response", slog.Error(err))
// Always provide a tool_result even if encoding failed.
errorJSON, _ := json.Marshal(i.newErrorResponse(err))
i.req.Messages = append(i.req.Messages, openai.ToolMessage(string(errorJSON), id))
continue
}
i.req.Messages = append(i.req.Messages, openai.ToolMessage(out.String(), id))
}
// Send termination marker.
if err := events.SendRaw(streamCtx, i.encodeForStream([]byte("[DONE]"))); err != nil {
logger.Debug(ctx, "failed to send termination marker", slog.Error(err))
}
// Give the events stream 30 seconds (TODO: configurable) to gracefully shutdown.
shutdownCtx, shutdownCancel := context.WithTimeout(ctx, time.Second*30)
defer shutdownCancel()
if err = events.Shutdown(shutdownCtx); err != nil {
logger.Warn(ctx, "event stream shutdown", slog.Error(err))
}
if err != nil {
streamCancel(xerrors.Errorf("stream err: %w", err))
} else {
streamCancel(xerrors.New("gracefully done"))
}
return interceptionErr
}
func (i *StreamingInterception) getInjectedToolByName(name string) *mcp.Tool {
if i.mcpProxy == nil {
return nil
}
return i.mcpProxy.GetTool(name)
}
// Mashals received stream chunk.
// Overrides id (since proxy obscures injected tool call invocations).
// If usage field was set in original chunk overrides it to culminative usage.
//
// sjson is used instead of normal struct marshaling so forwarded data
// is as close to the original as possible. Structs from openai library lack
// `omitzero/omitempty` annotations which adds additional empty fields
// when marshaling structs. Those additional empty fields can break Codex client.
func (i *StreamingInterception) marshalChunk(chunk *openai.ChatCompletionChunk, id uuid.UUID, prc *streamProcessor) ([]byte, error) {
sj, err := sjson.Set(chunk.RawJSON(), "id", id.String())
if err != nil {
return nil, xerrors.Errorf("marshal chunk id failed: %w", err)
}
// If usage information is available, relay the cumulative usage once all tool invocations have completed.
if chunk.JSON.Usage.Valid() {
u := prc.getCumulativeUsage()
sj, err = sjson.Set(sj, "usage", u)
if err != nil {
return nil, xerrors.Errorf("marshal chunk usage failed: %w", err)
}
}
return i.encodeForStream([]byte(sj)), nil
}
func (i *StreamingInterception) marshalErr(err error) ([]byte, error) {
data, err := json.Marshal(err)
if err != nil {
return nil, xerrors.Errorf("marshal error failed: %w", err)
}
return i.encodeForStream(data), nil
}
func (*StreamingInterception) encodeForStream(payload []byte) []byte {
// bytes.Buffer writes to in-memory storage and never return errors.
var buf bytes.Buffer
_, _ = buf.WriteString("data: ")
_, _ = buf.Write(payload)
_, _ = buf.WriteString("\n\n")
return buf.Bytes()
}
// newStream traces svc.NewStreaming(streamCtx, i.req.ChatCompletionNewParams) call
func (i *StreamingInterception) newStream(ctx context.Context, svc openai.ChatCompletionService, opts []option.RequestOption) *ssestream.Stream[openai.ChatCompletionChunk] {
_, span := i.tracer.Start(ctx, "Intercept.ProcessRequest.Upstream", trace.WithAttributes(tracing.InterceptionAttributesFromContext(ctx)...))
defer span.End()
return svc.NewStreaming(ctx, openai.ChatCompletionNewParams{}, opts...)
}
type streamProcessor struct {
ctx context.Context
logger slog.Logger
acc openai.ChatCompletionAccumulator
// Tool handling.
pendingToolCall bool
getInjectedToolFunc func(string) *mcp.Tool
// Token handling.
lastUsage openai.CompletionUsage
cumulativeUsage openai.CompletionUsage
}
func newStreamProcessor(ctx context.Context, logger slog.Logger, isToolInjectedFunc func(string) *mcp.Tool) *streamProcessor {
return &streamProcessor{
ctx: ctx,
logger: logger,
getInjectedToolFunc: isToolInjectedFunc,
}
}
// process receives a completion chunk and returns a bool indicating whether it should be
// relayed to the client.
func (s *streamProcessor) process(chunk openai.ChatCompletionChunk) bool {
if !s.acc.AddChunk(chunk) {
s.logger.Debug(s.ctx, "failed to accumulate chunk", slog.F("chunk", chunk.RawJSON()))
// Potentially not fatal, move along in best effort...
}
// Accumulate token usage.
s.lastUsage = chunk.Usage
s.cumulativeUsage = sumUsage(s.cumulativeUsage, chunk.Usage)
// If the stream has reached a terminal state (i.e. call a tool), and this tool is injected,
// then it must not be relayed.
if _, ok := s.acc.JustFinishedToolCall(); ok && s.pendingToolCall {
return false
}
if len(chunk.Choices) == 0 {
// Odd, should not occur, relay it on in case.
// Nothing more to be done.
return true
}
// We explicitly set n=1, so this shouldn't happen.
if count := len(chunk.Choices); count > 1 {
s.logger.Warn(s.ctx, "multiple choices returned, only handling first", slog.F("count", count))
}
// Check if we have a tool call in progress.
//
// The API will send partial tool call events like this:
//
// data: ... delta":{"tool_calls":[{"index":0,"id":"call_0TxntkwDB66KH8z4RwNqeWrZ","type":"function","function":{"name":"bmcp_coder_coder_list_workspaces","arguments":""}}]}...
// data: ... delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]}...
// data: ... delta":{"tool_calls":[{"index":0,"function":{"arguments":"owner"}}]}...
// data: ... delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]}...
// data: ... delta":{"tool_calls":[{"index":0,"function":{"arguments":"admin"}}]}...
// data: ... delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]}...
//
// So we need to ensure that we don't relay any of the partial events to the client in the case of
// an injected tool.
//
// The first partial will tell us the tool name, and we can then decide how to proceed.
choice := chunk.Choices[0]
if len(choice.Delta.ToolCalls) == 0 {
// No tool calls, no special handling required.
return true
}
// If we have a pending injected tool call in progress, do not relay any subsequent partial chunks.
if s.pendingToolCall {
return false
}
// This shouldn't happen since we have parallel tool calls disabled currently.
if count := len(choice.Delta.ToolCalls); count > 1 {
s.logger.Warn(context.Background(), "unexpected tool call count", slog.F("count", count))
// We'll continue and just examine the first tool.
}
toolCall := choice.Delta.ToolCalls[0]
if s.isInjected(toolCall) {
// Mark tool as pending until tool call is finished.
s.pendingToolCall = true
return false
}
// There is a tool call, but it's not injected.
return true
}
// getMsgID returns the ID given by the API for this (accumulated) message.
func (s *streamProcessor) getMsgID() string {
return s.acc.ID
}
func (s *streamProcessor) isInjected(toolCall openai.ChatCompletionChunkChoiceDeltaToolCall) bool {
return s.getInjectedToolFunc(strings.TrimSpace(toolCall.Function.Name)) != nil
}
func (s *streamProcessor) getToolCall() *openai.FinishedChatCompletionToolCall {
tc, ok := s.acc.JustFinishedToolCall()
if !ok {
return nil
}
return &tc
}
func (s *streamProcessor) getLastCompletion() *openai.ChatCompletionMessage {
if len(s.acc.Choices) == 0 {
return nil
}
return &s.acc.Choices[0].Message
}
func (s *streamProcessor) getLastUsage() openai.CompletionUsage {
return s.lastUsage
}
func (s *streamProcessor) getCumulativeUsage() openai.CompletionUsage {
return s.cumulativeUsage
}
// compactToolCalls removes nil/empty tool call entries (without an ID).
func compactToolCalls(msg *openai.ChatCompletionMessage) {
if msg == nil || len(msg.ToolCalls) == 0 {
return
}
msg.ToolCalls = slices.DeleteFunc(msg.ToolCalls, func(tc openai.ChatCompletionMessageToolCallUnion) bool {
return tc.ID == ""
})
}
@@ -0,0 +1,112 @@
package chatcompletions_test
import (
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/google/uuid"
"github.com/openai/openai-go/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"cdr.dev/slog/v3"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/aibridge/config"
"github.com/coder/coder/v2/aibridge/intercept"
"github.com/coder/coder/v2/aibridge/intercept/chatcompletions"
"github.com/coder/coder/v2/aibridge/internal/testutil"
)
// Test that when the upstream provider returns an error before streaming starts,
// the error status code and body are correctly relayed to the client.
func TestStreamingInterception_RelaysUpstreamErrorToClient(t *testing.T) {
t.Parallel()
tests := []struct {
name string
statusCode int
responseBody string
expectedErrStr string
expectedBody string
}{
{
name: "bad request error",
statusCode: http.StatusBadRequest,
responseBody: `{"error":{"message":"Invalid request","type":"invalid_request_error","code":"invalid_request"}}`,
expectedErrStr: strconv.Itoa(http.StatusBadRequest),
expectedBody: "invalid_request",
},
{
name: "rate limit error",
statusCode: http.StatusTooManyRequests,
responseBody: `{"error":{"message":"Rate limit exceeded","type":"rate_limit_error","code":"rate_limit_exceeded"}}`,
expectedErrStr: strconv.Itoa(http.StatusTooManyRequests),
expectedBody: "rate_limit",
},
{
name: "internal server error",
statusCode: http.StatusInternalServerError,
responseBody: `{"error":{"message":"Internal server error","type":"server_error","code":"internal_error"}}`,
expectedErrStr: strconv.Itoa(http.StatusInternalServerError),
expectedBody: "server_error",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Setup a mock server that returns an error immediately (before any streaming)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("x-should-retry", "false")
w.WriteHeader(tc.statusCode)
_, _ = w.Write([]byte(tc.responseBody))
}))
t.Cleanup(mockServer.Close)
// Create interceptor with mock server URL
cfg := config.OpenAI{
BaseURL: mockServer.URL,
Key: "test-key",
}
req := &chatcompletions.ChatCompletionNewParamsWrapper{
ChatCompletionNewParams: openai.ChatCompletionNewParams{
Model: "gpt-4",
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("hello"),
},
},
Stream: true,
}
// Create test request
w := httptest.NewRecorder()
httpReq := httptest.NewRequest(http.MethodPost, "/chat/completions", nil)
tracer := otel.Tracer("test")
interceptor := chatcompletions.NewStreamingInterceptor(uuid.New(), req, config.ProviderOpenAI, cfg, httpReq.Header, "Authorization", tracer, intercept.CredentialInfo{})
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
interceptor.Setup(logger, &testutil.MockRecorder{}, nil)
// Process the request
err := interceptor.ProcessRequest(w, httpReq)
// Verify error was returned
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedErrStr)
// Verify status code was written to response
assert.Equal(t, tc.statusCode, w.Code, "expected status code to be relayed to client")
// Verify error body contains expected error info
body := w.Body.String()
assert.Contains(t, body, tc.expectedBody, "expected error type in response body")
})
}
}
+74
View File
@@ -0,0 +1,74 @@
package intercept
import (
"net/http"
)
// hopByHopHeaders are connection-level headers specific to the connection
// between client and AI Bridge, not meant for the upstream.
// See https://www.rfc-editor.org/rfc/rfc2616#section-13.5.1
var hopByHopHeaders = []string{
"Connection",
"Keep-Alive",
"Proxy-Authenticate",
"Proxy-Authorization",
"Te",
"Trailer",
"Transfer-Encoding",
"Upgrade",
}
// nonForwardedHeaders are transport-level headers managed by aibridge or
// Go's HTTP transport that must not be forwarded to the upstream provider.
var nonForwardedHeaders = []string{
"Host",
"Accept-Encoding",
"Content-Length",
}
// authHeaders are headers that carry authentication credentials from the
// client. The upstream request is built by the SDK, which sets the correct
// provider credentials via option.WithAPIKey. Client auth headers are
// stripped here and the provider credentials are re-injected by
// BuildUpstreamHeaders from the SDK-built request.
var authHeaders = []string{
"Authorization",
"X-Api-Key",
}
// PrepareClientHeaders returns a copy of the client headers with hop-by-hop,
// transport, and auth headers removed.
func PrepareClientHeaders(clientHeaders http.Header) http.Header {
prepared := clientHeaders.Clone()
for _, h := range hopByHopHeaders {
prepared.Del(h)
}
for _, h := range nonForwardedHeaders {
prepared.Del(h)
}
for _, h := range authHeaders {
prepared.Del(h)
}
return prepared
}
// BuildUpstreamHeaders produces the header set for an upstream SDK request.
// It starts from the prepared client headers, then preserves specific
// headers from the SDK-built request that must not be overwritten.
func BuildUpstreamHeaders(sdkHeader http.Header, clientHeaders http.Header, authHeaderName string) http.Header {
headers := PrepareClientHeaders(clientHeaders)
// Preserve the auth header set by the SDK from the provider configuration.
if v := sdkHeader.Get(authHeaderName); v != "" {
headers.Set(authHeaderName, v)
}
// Preserve actor headers injected by aibridge as per-request SDK options.
for name, values := range sdkHeader {
if IsActorHeader(name) {
headers[name] = values
}
}
return headers
}
+221
View File
@@ -0,0 +1,221 @@
package intercept_test
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/aibridge/intercept"
)
func TestPrepareClientHeaders(t *testing.T) {
t.Parallel()
t.Run("nil input returns empty header", func(t *testing.T) {
t.Parallel()
result := intercept.PrepareClientHeaders(nil)
require.Empty(t, result)
})
t.Run("hop-by-hop headers are removed", func(t *testing.T) {
t.Parallel()
input := http.Header{
"Connection": {"keep-alive"},
"Keep-Alive": {"timeout=5"},
"Transfer-Encoding": {"chunked"},
"Upgrade": {"websocket"},
"X-Custom": {"preserved"},
}
result := intercept.PrepareClientHeaders(input)
assert.Empty(t, result.Get("Connection"))
assert.Empty(t, result.Get("Keep-Alive"))
assert.Empty(t, result.Get("Transfer-Encoding"))
assert.Empty(t, result.Get("Upgrade"))
assert.Equal(t, "preserved", result.Get("X-Custom"))
})
t.Run("non-forwarded headers are removed", func(t *testing.T) {
t.Parallel()
input := http.Header{
"Host": {"example.com"},
"Accept-Encoding": {"gzip"},
"Content-Length": {"42"},
"X-Custom": {"preserved"},
}
result := intercept.PrepareClientHeaders(input)
assert.Empty(t, result.Get("Host"))
assert.Empty(t, result.Get("Accept-Encoding"))
assert.Empty(t, result.Get("Content-Length"))
assert.Equal(t, "preserved", result.Get("X-Custom"))
})
t.Run("auth headers are removed", func(t *testing.T) {
t.Parallel()
input := http.Header{
"Authorization": {"Bearer coder-session-token"},
"X-Api-Key": {"sk-client-key"},
"X-Custom": {"preserved"},
}
result := intercept.PrepareClientHeaders(input)
assert.Empty(t, result.Get("Authorization"))
assert.Empty(t, result.Get("X-Api-Key"))
assert.Equal(t, "preserved", result.Get("X-Custom"))
})
t.Run("multi-value headers are preserved", func(t *testing.T) {
t.Parallel()
input := http.Header{
"X-Custom": {"value-1", "value-2"},
}
result := intercept.PrepareClientHeaders(input)
require.Equal(t, []string{"value-1", "value-2"}, result["X-Custom"])
})
t.Run("input is not mutated", func(t *testing.T) {
t.Parallel()
input := http.Header{
"Connection": {"keep-alive"},
"X-Custom": {"preserved"},
}
originalCopy := input.Clone()
_ = intercept.PrepareClientHeaders(input)
require.Equal(t, originalCopy, input)
})
}
func TestBuildUpstreamHeaders(t *testing.T) {
t.Parallel()
t.Run("preserves auth from SDK", func(t *testing.T) {
t.Parallel()
sdkHeader := http.Header{
"Authorization": {"Bearer sk-provider-key"},
}
clientHeaders := http.Header{
"Authorization": {"Bearer coder-session-token"},
"User-Agent": {"claude-code/1.0"},
}
result := intercept.BuildUpstreamHeaders(sdkHeader, clientHeaders, "Authorization")
assert.Equal(t, "Bearer sk-provider-key", result.Get("Authorization"))
assert.Equal(t, "claude-code/1.0", result.Get("User-Agent"))
})
t.Run("preserves X-Api-Key from SDK and strips client Authorization", func(t *testing.T) {
t.Parallel()
sdkHeader := http.Header{
"X-Api-Key": {"sk-ant-provider-key"},
}
clientHeaders := http.Header{
"X-Api-Key": {"sk-ant-client-key"},
"Authorization": {"Bearer coder-session-token"},
"Anthropic-Beta": {"prompt-caching-2024-07-31"},
}
result := intercept.BuildUpstreamHeaders(sdkHeader, clientHeaders, "X-Api-Key")
assert.Equal(t, "sk-ant-provider-key", result.Get("X-Api-Key"))
assert.Empty(t, result.Get("Authorization"))
assert.Equal(t, "prompt-caching-2024-07-31", result.Get("Anthropic-Beta"))
})
t.Run("preserves actor headers from SDK", func(t *testing.T) {
t.Parallel()
sdkHeader := http.Header{
"Authorization": {"Bearer sk-key"},
"X-Ai-Bridge-Actor-Id": {"user-123"},
"X-Ai-Bridge-Actor-Metadata-Name": {"alice"},
}
clientHeaders := http.Header{
"Authorization": {"Bearer coder-token"},
"User-Agent": {"claude-code/1.0"},
}
result := intercept.BuildUpstreamHeaders(sdkHeader, clientHeaders, "Authorization")
assert.Equal(t, "Bearer sk-key", result.Get("Authorization"))
assert.Equal(t, "user-123", result.Get("X-Ai-Bridge-Actor-Id"))
assert.Equal(t, "alice", result.Get("X-Ai-Bridge-Actor-Metadata-Name"))
assert.Equal(t, "claude-code/1.0", result.Get("User-Agent"))
})
t.Run("strips hop-by-hop and transport headers", func(t *testing.T) {
t.Parallel()
sdkHeader := http.Header{
"Authorization": {"Bearer sk-key"},
}
clientHeaders := http.Header{
"Connection": {"keep-alive"},
"Host": {"bridge.example.com"},
"Content-Length": {"99"},
"Accept-Encoding": {"gzip"},
"Transfer-Encoding": {"chunked"},
"User-Agent": {"claude-code/1.0"},
}
result := intercept.BuildUpstreamHeaders(sdkHeader, clientHeaders, "Authorization")
assert.Empty(t, result.Get("Connection"))
assert.Empty(t, result.Get("Host"))
assert.Empty(t, result.Get("Content-Length"))
assert.Empty(t, result.Get("Accept-Encoding"))
assert.Empty(t, result.Get("Transfer-Encoding"))
assert.Equal(t, "claude-code/1.0", result.Get("User-Agent"))
})
t.Run("empty auth header in SDK is not injected", func(t *testing.T) {
t.Parallel()
sdkHeader := http.Header{}
clientHeaders := http.Header{
"User-Agent": {"claude-code/1.0"},
}
result := intercept.BuildUpstreamHeaders(sdkHeader, clientHeaders, "Authorization")
assert.Empty(t, result.Get("Authorization"))
assert.Equal(t, "claude-code/1.0", result.Get("User-Agent"))
})
t.Run("does not mutate inputs", func(t *testing.T) {
t.Parallel()
sdkHeader := http.Header{
"Authorization": {"Bearer sk-key"},
}
clientHeaders := http.Header{
"Authorization": {"Bearer coder-token"},
"Connection": {"keep-alive"},
}
sdkCopy := sdkHeader.Clone()
clientCopy := clientHeaders.Clone()
_ = intercept.BuildUpstreamHeaders(sdkHeader, clientHeaders, "Authorization")
require.Equal(t, sdkCopy, sdkHeader)
require.Equal(t, clientCopy, clientHeaders)
})
}
+31
View File
@@ -0,0 +1,31 @@
package intercept
import "github.com/coder/coder/v2/aibridge/utils"
// CredentialKind identifies how a request was authenticated.
// Keep in sync with the credential_kind enum in coderd's database.
type CredentialKind string
// Credential kind constants for interception recording.
const (
CredentialKindCentralized CredentialKind = "centralized"
CredentialKindBYOK CredentialKind = "byok"
)
// CredentialInfo holds credential metadata for an interception.
type CredentialInfo struct {
Kind CredentialKind
Hint string
Length int
}
// NewCredentialInfo creates a CredentialInfo from a raw credential.
// The credential is automatically masked before storage so that the
// original secret is never retained.
func NewCredentialInfo(kind CredentialKind, credential string) CredentialInfo {
return CredentialInfo{
Kind: kind,
Hint: utils.MaskSecret(credential),
Length: len(credential),
}
}
@@ -0,0 +1,266 @@
package eventstream
import (
"context"
"errors"
"io"
"net"
"net/http"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/quartz"
)
var ErrEventStreamClosed = xerrors.New("event stream closed")
const (
pingInterval = time.Second * 10
// SlowFlushThreshold is the duration after which a flush to the client is
// considered slow and a warning is logged.
SlowFlushThreshold = time.Millisecond * 500
)
type event []byte
type EventStream struct {
ctx context.Context
logger slog.Logger
clk quartz.Clock
pingPayload []byte
initiated atomic.Bool
initiateOnce sync.Once
shutdownOnce sync.Once
eventsCh chan event
// doneCh is closed when the start loop exits.
doneCh chan struct{}
// tick sends periodic pings to keep the connection alive.
tick *time.Ticker
}
// NewEventStream creates a new SSE stream, with an optional payload which is used to send pings every [pingInterval].
func NewEventStream(ctx context.Context, logger slog.Logger, pingPayload []byte, clk quartz.Clock) *EventStream {
// Send periodic pings to keep connections alive.
// The upstream provider may also send their own pings, but we can't rely on this.
tick := time.NewTicker(time.Nanosecond)
tick.Stop() // Ticker will start after stream initiation.
return &EventStream{
ctx: ctx,
logger: logger,
clk: clk,
pingPayload: pingPayload,
eventsCh: make(chan event, 128), // Small buffer to unblock senders; once full, senders will block.
doneCh: make(chan struct{}),
tick: tick,
}
}
// InitiateStream initiates the SSE stream by sending headers and starting the
// ping ticker. This is safe to call multiple times as only the first call has
// any effect.
func (s *EventStream) InitiateStream(w http.ResponseWriter) {
s.initiateOnce.Do(func() {
s.initiated.Store(true)
s.logger.Debug(s.ctx, "stream initiated")
// Send headers for Server-Sent Event stream.
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no")
// Send initial flush to ensure connection is established.
if err := flush(w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Start ping ticker.
s.tick.Reset(pingInterval)
})
}
// Start handles sending Server-Sent Event to the client.
func (s *EventStream) Start(w http.ResponseWriter, r *http.Request) {
// Signal completion on exit so senders don't block indefinitely after closure.
defer close(s.doneCh)
ctx := r.Context()
defer s.tick.Stop()
for {
var (
ev event
open bool
)
select {
case <-s.ctx.Done():
return
case <-ctx.Done():
s.logger.Debug(ctx, "request context canceled", slog.Error(ctx.Err()))
return
case ev, open = <-s.eventsCh: // Once closed, the buffered channel will drain all buffered values before showing as closed.
if !open {
s.logger.Debug(ctx, "events channel closed")
return
}
// Initiate the stream on first event (if not already initiated).
s.InitiateStream(w)
case <-s.tick.C:
ev = s.pingPayload
if ev == nil {
continue
}
}
_, err := w.Write(ev)
if err != nil {
if IsConnError(err) {
s.logger.Debug(ctx, "client disconnected during SSE write", slog.Error(err))
} else {
s.logger.Warn(ctx, "failed to write SSE event", slog.Error(err))
}
return
}
flushStart := s.clk.Now()
if err := flush(w); err != nil {
s.logger.Warn(ctx, "failed to flush event stream", slog.Error(err))
return
}
if d := s.clk.Since(flushStart); d > SlowFlushThreshold {
clientIP, _, _ := net.SplitHostPort(r.RemoteAddr)
s.logger.Warn(ctx, "slow client detected",
slog.F("flush_duration", d),
slog.F("client_ip", clientIP),
slog.F("user_agent", r.Header.Get("User-Agent")),
slog.F("payload_size", len(ev)),
)
}
// Reset the timer once we've flushed some data to the stream, since it's already fresh.
// No need to ping in that case.
s.tick.Reset(pingInterval)
}
}
// Send enqueues an event in a non-blocking fashion, but if the channel is full
// then it will block.
func (s *EventStream) Send(ctx context.Context, payload []byte) error {
// Save an unnecessary marshaling if possible.
select {
case <-ctx.Done():
return ctx.Err()
case <-s.ctx.Done():
return s.ctx.Err()
case <-s.doneCh:
return ErrEventStreamClosed
default:
}
return s.SendRaw(ctx, payload)
}
func (s *EventStream) SendRaw(ctx context.Context, payload []byte) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-s.ctx.Done():
return s.ctx.Err()
case <-s.doneCh:
return ErrEventStreamClosed
case s.eventsCh <- payload:
return nil
}
}
// Shutdown gracefully shuts down the stream, sending any supplementary events downstream if required.
// ONLY call this once all events have been submitted.
func (s *EventStream) Shutdown(shutdownCtx context.Context) error {
s.shutdownOnce.Do(func() {
s.logger.Debug(shutdownCtx, "shutdown initiated", slog.F("outstanding_events", len(s.eventsCh)))
// Now it is safe to close the events channel; the Start() loop will exit
// after draining remaining events and receivers will stop ranging.
close(s.eventsCh)
})
var err error
select {
case <-shutdownCtx.Done():
// If shutdownCtx completes, shutdown likely exceeded its timeout.
err = xerrors.Errorf("shutdown ended prematurely with %d outstanding events: %w", len(s.eventsCh), shutdownCtx.Err())
case <-s.ctx.Done():
err = xerrors.Errorf("shutdown ended prematurely with %d outstanding events: %w", len(s.eventsCh), s.ctx.Err())
case <-s.doneCh:
return nil
}
// Even if the context is canceled, we need to wait for Start() to complete.
<-s.doneCh
return err
}
// IsStreaming checks if the stream has been initiated, or
// when events are buffered which - when processed - will initiate the stream.
func (s *EventStream) IsStreaming() bool {
return s.initiated.Load() || len(s.eventsCh) > 0
}
// IsConnError checks if an error is related to client disconnection or context cancellation.
func IsConnError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, io.EOF) {
return true
}
if errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.EPIPE) || errors.Is(err, net.ErrClosed) {
return true
}
errStr := err.Error()
return strings.Contains(errStr, "broken pipe") ||
strings.Contains(errStr, "connection reset by peer")
}
func IsUnrecoverableError(err error) bool {
if errors.Is(err, context.Canceled) {
return true
}
return IsConnError(err)
}
func flush(w http.ResponseWriter) (err error) {
flusher, ok := w.(http.Flusher)
if !ok || flusher == nil {
return xerrors.New("SSE not supported")
}
defer func() {
if r := recover(); r != nil { //nolint:revive,staticcheck // Intentionally swallowed; likely a broken connection.
}
}()
flusher.Flush()
return nil
}
@@ -0,0 +1,110 @@
package eventstream_test
import (
"bufio"
"context"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3"
"cdr.dev/slog/v3/sloggers/sloghuman"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/aibridge/intercept/eventstream"
"github.com/coder/quartz"
)
// clockAdvancingFlusher wraps httptest.ResponseRecorder and advances the mock
// clock on each Flush call, simulating a slow client without real sleeping.
type clockAdvancingFlusher struct {
*httptest.ResponseRecorder
clk *quartz.Mock
advance time.Duration
}
func (f *clockAdvancingFlusher) Flush() {
f.clk.Advance(f.advance)
f.ResponseRecorder.Flush()
}
// Hijack satisfies the FullResponseWriter lint rule.
func (*clockAdvancingFlusher) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return nil, nil, nil
}
func TestEventStream_LogsWarning_WhenFlushIsSlow(t *testing.T) {
t.Parallel()
var buf strings.Builder
logger := slogtest.Make(t, nil).AppendSinks(sloghuman.Sink(&buf)).Leveled(slog.LevelWarn)
ctx := context.Background()
clk := quartz.NewMock(t)
stream := eventstream.NewEventStream(ctx, logger, nil, clk)
w := &clockAdvancingFlusher{
ResponseRecorder: httptest.NewRecorder(),
clk: clk,
advance: eventstream.SlowFlushThreshold + time.Millisecond,
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/", nil)
require.NoError(t, err)
req.RemoteAddr = "192.0.2.1:12345"
req.Header.Set("User-Agent", "test-agent/1.0")
done := make(chan struct{})
go func() {
defer close(done)
stream.Start(w, req)
}()
stream.InitiateStream(w)
require.NoError(t, stream.SendRaw(ctx, []byte("data: hello\n\n")))
require.NoError(t, stream.Shutdown(ctx))
<-done
require.Contains(t, buf.String(), "slow client detected")
require.Contains(t, buf.String(), "192.0.2.1")
require.Contains(t, buf.String(), "test-agent/1.0")
require.Contains(t, buf.String(), "payload_size=13")
}
func TestEventStream_NoWarning_WhenFlushIsFast(t *testing.T) {
t.Parallel()
var buf strings.Builder
logger := slogtest.Make(t, nil).AppendSinks(sloghuman.Sink(&buf)).Leveled(slog.LevelWarn)
ctx := context.Background()
clk := quartz.NewMock(t)
stream := eventstream.NewEventStream(ctx, logger, nil, clk)
// No clock advance, flush duration stays at 0, below threshold.
w := &clockAdvancingFlusher{
ResponseRecorder: httptest.NewRecorder(),
clk: clk,
advance: 0,
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/", nil)
require.NoError(t, err)
done := make(chan struct{})
go func() {
defer close(done)
stream.Start(w, req)
}()
stream.InitiateStream(w)
require.NoError(t, stream.SendRaw(ctx, []byte("data: hello\n\n")))
require.NoError(t, stream.Shutdown(ctx))
<-done
require.Empty(t, buf.String())
}
+40
View File
@@ -0,0 +1,40 @@
package intercept
import (
"net/http"
"github.com/google/uuid"
"go.opentelemetry.io/otel/attribute"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/mcp"
"github.com/coder/coder/v2/aibridge/recorder"
)
// Interceptor describes a (potentially) stateful interaction with an AI provider.
type Interceptor interface {
// ID returns the unique identifier for this interception.
ID() uuid.UUID
// Setup injects some required dependencies. This MUST be called before using the interceptor
// to process requests.
Setup(logger slog.Logger, rec recorder.Recorder, mcpProxy mcp.ServerProxier)
// Model returns the model in use for this [Interceptor].
Model() string
// ProcessRequest handles the HTTP request.
ProcessRequest(w http.ResponseWriter, r *http.Request) error
// Specifies whether an interceptor handles streaming or not.
Streaming() bool
// TraceAttributes returns tracing attributes for this [Interceptor]
TraceAttributes(*http.Request) []attribute.KeyValue
// Credential returns the credential metadata for this interception.
Credential() CredentialInfo
// CorrelatingToolCallID returns the ID of a tool call result submitted
// in the request, if present. This is used to correlate the current
// interception back to the previous interception that issued those tool
// calls. If multiple tool use results are present, we use the last one
// (most recent). Both Anthropic's /v1/messages and OpenAI's /v1/responses
// require that ALL tool results are submitted for tool choices returned
// by the model, so any single tool call ID is sufficient to identify the
// parent interception.
CorrelatingToolCallID() *string
}
+563
View File
@@ -0,0 +1,563 @@
package messages
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/bedrock"
"github.com/anthropics/anthropic-sdk-go/option"
"github.com/anthropics/anthropic-sdk-go/shared"
"github.com/anthropics/anthropic-sdk-go/shared/constant"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/google/uuid"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
aibconfig "github.com/coder/coder/v2/aibridge/config"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/intercept"
"github.com/coder/coder/v2/aibridge/intercept/apidump"
"github.com/coder/coder/v2/aibridge/mcp"
"github.com/coder/coder/v2/aibridge/recorder"
"github.com/coder/coder/v2/aibridge/tracing"
"github.com/coder/coder/v2/aibridge/utils"
"github.com/coder/quartz"
)
// bedrockSupportedBetaFlags is the set of Anthropic-Beta flags that AWS Bedrock
// accepts. Flags not in this set cause a 400 "invalid beta flag" error.
//
// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages-request-response.html
var bedrockSupportedBetaFlags = map[string]bool{
// Supported on Claude 3.7 Sonnet.
"computer-use-2025-01-24": true,
// Supported on Claude 3.7 Sonnet and Claude 4+.
"token-efficient-tools-2025-02-19": true,
// Supported on Claude 4+ models.
"interleaved-thinking-2025-05-14": true,
// Supported on Claude 3.7 Sonnet.
"output-128k-2025-02-19": true,
// Supported on Claude 4+ models. Requires account team access.
"dev-full-thinking-2025-05-14": true,
// Supported on Claude Sonnet 4.
"context-1m-2025-08-07": true,
// Supported on Claude Sonnet 4.5 and Claude Haiku 4.5.
// Enables context_management body field for thinking block clearing.
"context-management-2025-06-27": true,
// Supported on Claude Opus 4.5.
// Enables output_config body field for effort control.
"effort-2025-11-24": true,
// Supported on Claude Opus 4.5.
"tool-search-tool-2025-10-19": true,
// Supported on Claude Opus 4.5.
"tool-examples-2025-10-29": true,
}
type interceptionBase struct {
id uuid.UUID
providerName string
reqPayload RequestPayload
cfg aibconfig.Anthropic
bedrockCfg *aibconfig.AWSBedrock
// clientHeaders are the original HTTP headers from the client request.
clientHeaders http.Header
authHeaderName string
tracer trace.Tracer
logger slog.Logger
recorder recorder.Recorder
mcpProxy mcp.ServerProxier
credential intercept.CredentialInfo
}
func (i *interceptionBase) ID() uuid.UUID {
return i.id
}
func (i *interceptionBase) Credential() intercept.CredentialInfo {
return i.credential
}
func (i *interceptionBase) Setup(logger slog.Logger, rec recorder.Recorder, mcpProxy mcp.ServerProxier) {
i.logger = logger
i.recorder = rec
i.mcpProxy = mcpProxy
}
func (i *interceptionBase) CorrelatingToolCallID() *string {
return i.reqPayload.correlatingToolCallID()
}
func (i *interceptionBase) Model() string {
if len(i.reqPayload) == 0 {
return "coder-aibridge-unknown"
}
if i.bedrockCfg != nil {
model := i.bedrockCfg.Model
if i.isSmallFastModel() {
model = i.bedrockCfg.SmallFastModel
}
return model
}
return i.reqPayload.model()
}
func (i *interceptionBase) baseTraceAttributes(r *http.Request, streaming bool) []attribute.KeyValue {
return []attribute.KeyValue{
attribute.String(tracing.RequestPath, r.URL.Path),
attribute.String(tracing.InterceptionID, i.id.String()),
attribute.String(tracing.InitiatorID, aibcontext.ActorIDFromContext(r.Context())),
attribute.String(tracing.Provider, i.providerName),
attribute.String(tracing.Model, i.Model()),
attribute.Bool(tracing.Streaming, streaming),
attribute.Bool(tracing.IsBedrock, i.bedrockCfg != nil),
}
}
func (i *interceptionBase) injectTools() {
if i.mcpProxy == nil || !i.hasInjectableTools() {
return
}
i.disableParallelToolCalls()
// Inject tools.
var injectedTools []anthropic.ToolUnionParam
for _, tool := range i.mcpProxy.ListTools() {
injectedTools = append(injectedTools, anthropic.ToolUnionParam{
OfTool: &anthropic.ToolParam{
InputSchema: anthropic.ToolInputSchemaParam{
Properties: tool.Params,
Required: tool.Required,
},
Name: tool.ID,
Description: anthropic.String(tool.Description),
Type: anthropic.ToolTypeCustom,
},
})
}
// Prepend the injected tools in order to maintain any configured cache breakpoints.
// The order of injected tools is expected to be stable, and therefore will not cause
// any cache invalidation when prepended.
updated, err := i.reqPayload.injectTools(injectedTools)
if err != nil {
i.logger.Warn(context.Background(), "failed to set inject tools in request payload", slog.Error(err))
return
}
i.reqPayload = updated
}
func (i *interceptionBase) disableParallelToolCalls() {
// Note: Parallel tool calls are disabled to avoid tool_use/tool_result block mismatches.
// https://github.com/coder/aibridge/issues/2
updated, err := i.reqPayload.disableParallelToolCalls()
if err != nil {
i.logger.Warn(context.Background(), "failed to set tool_choice in request payload", slog.Error(err))
return
}
i.reqPayload = updated
}
// extractModelThoughts returns any thinking blocks that were returned in the response.
func (*interceptionBase) extractModelThoughts(msg *anthropic.Message) []*recorder.ModelThoughtRecord {
if msg == nil {
return nil
}
var thoughtRecords []*recorder.ModelThoughtRecord
for _, block := range msg.Content {
// anthropic.RedactedThinkingBlock also exists, but there's nothing useful we can capture.
variant, ok := block.AsAny().(anthropic.ThinkingBlock)
if !ok || variant.Thinking == "" {
continue
}
thoughtRecords = append(thoughtRecords, &recorder.ModelThoughtRecord{
Content: variant.Thinking,
Metadata: recorder.Metadata{"source": recorder.ThoughtSourceThinking},
})
}
return thoughtRecords
}
// IsSmallFastModel checks if the model is a small/fast model (Haiku 3.5).
// These models are optimized for tasks like code autocomplete and other small, quick operations.
// See `ANTHROPIC_SMALL_FAST_MODEL`: https://docs.anthropic.com/en/docs/claude-code/settings#environment-variables
// https://docs.claude.com/en/docs/claude-code/costs#background-token-usage
func (i *interceptionBase) isSmallFastModel() bool {
return strings.Contains(i.reqPayload.model(), "haiku")
}
func (i *interceptionBase) newMessagesService(ctx context.Context, opts ...option.RequestOption) (anthropic.MessageService, error) {
// BYOK with access token uses Authorization: Bearer.
// Otherwise use X-Api-Key (centralized or BYOK with personal API key).
if i.cfg.BYOKBearerToken != "" {
i.logger.Debug(ctx, "using byok access token auth",
slog.F("bearer_hint", utils.MaskSecret(i.cfg.BYOKBearerToken)),
)
opts = append(opts, option.WithAuthToken(i.cfg.BYOKBearerToken))
} else {
i.logger.Debug(ctx, "using api key auth",
slog.F("api_key_hint", utils.MaskSecret(i.cfg.Key)),
)
opts = append(opts, option.WithAPIKey(i.cfg.Key))
}
opts = append(opts, option.WithBaseURL(i.cfg.BaseURL))
if i.cfg.MaxRetries != nil {
opts = append(opts, option.WithMaxRetries(*i.cfg.MaxRetries))
}
// Add extra headers if configured.
// Some providers require additional headers that are not added by the SDK.
// TODO(ssncferreira): remove as part of https://github.com/coder/aibridge/issues/192
for key, value := range i.cfg.ExtraHeaders {
opts = append(opts, option.WithHeader(key, value))
}
// Forward client headers to upstream. This middleware runs after the SDK
// has built the request, and replaces the outgoing headers with the sanitized
// client headers plus provider auth.
if i.clientHeaders != nil {
opts = append(opts, option.WithMiddleware(func(req *http.Request, next option.MiddlewareNext) (*http.Response, error) {
req.Header = intercept.BuildUpstreamHeaders(req.Header, i.clientHeaders, i.authHeaderName)
return next(req)
}))
}
// Add API dump middleware if configured
if mw := apidump.NewBridgeMiddleware(i.cfg.APIDumpDir, i.providerName, i.Model(), i.id, i.logger, quartz.NewReal()); mw != nil {
opts = append(opts, option.WithMiddleware(mw))
}
if i.bedrockCfg != nil {
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()
bedrockOpts, err := i.withAWSBedrockOptions(ctx, i.bedrockCfg)
if err != nil {
return anthropic.MessageService{}, err
}
opts = append(opts, bedrockOpts...)
i.augmentRequestForBedrock()
}
return anthropic.NewMessageService(opts...), nil
}
// withBody returns a per-request option that sends the current raw request
// payload as the request body. This is called for each API request so that the
// latest payload (including any messages appended during the agentic tool loop)
// is always sent.
func (i *interceptionBase) withBody() option.RequestOption {
return option.WithRequestBody("application/json", []byte(i.reqPayload))
}
// withAWSBedrockOptions returns request options for authenticating with AWS Bedrock.
//
// When both AccessKey and AccessKeySecret are set in the aibridge config, they are
// used directly as static credentials. Otherwise, the AWS SDK default credential chain
// resolves credentials (environment variables, shared config/credentials files, IAM
// roles, IRSA, SSO, IMDS, etc.).
func (*interceptionBase) withAWSBedrockOptions(ctx context.Context, cfg *aibconfig.AWSBedrock) ([]option.RequestOption, error) {
if cfg == nil {
return nil, xerrors.New("nil config given")
}
if cfg.Region == "" && cfg.BaseURL == "" {
return nil, xerrors.New("region or base url required")
}
if cfg.Model == "" {
return nil, xerrors.New("model required")
}
if cfg.SmallFastModel == "" {
return nil, xerrors.New("small fast model required")
}
loadOpts := []func(*config.LoadOptions) error{
config.WithRegion(cfg.Region),
}
// Use static credentials when explicitly provided, otherwise fall back to the SDK default credential chain.
switch {
// Both set: use static credentials directly.
case cfg.AccessKey != "" && cfg.AccessKeySecret != "":
loadOpts = append(loadOpts, config.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(
cfg.AccessKey,
cfg.AccessKeySecret,
"",
),
))
// Only one set: misconfiguration.
case cfg.AccessKey != "" || cfg.AccessKeySecret != "":
return nil, xerrors.New("both access key and access key secret must be provided together")
// Neither set: SDK default credential chain resolves credentials.
default:
}
awsCfg, err := config.LoadDefaultConfig(ctx, loadOpts...)
if err != nil {
return nil, xerrors.Errorf("failed to load AWS Bedrock config: %w", err)
}
// Fail fast: ensure credentials can be resolved before making any requests.
// awsCfg already carries the credentials provider, and the Bedrock middleware
// will call Retrieve on it when signing each request.
if _, err := awsCfg.Credentials.Retrieve(ctx); err != nil {
return nil, xerrors.Errorf("no AWS credentials found: %w", err)
}
var out []option.RequestOption
out = append(out, bedrock.WithConfig(awsCfg))
// If a custom base URL is set, override the default endpoint constructed by the bedrock middleware.
if cfg.BaseURL != "" {
out = append(out, option.WithBaseURL(cfg.BaseURL))
}
return out, nil
}
// augmentRequestForBedrock will change the model used for the request since AWS Bedrock doesn't support
// Anthropics' model names. It also converts adaptive thinking to enabled with a budget for models that
// don't support adaptive thinking natively.
func (i *interceptionBase) augmentRequestForBedrock() {
if i.bedrockCfg == nil {
return
}
model := i.Model()
updated, err := i.reqPayload.withModel(model)
if err != nil {
i.logger.Warn(context.Background(), "failed to set model in request payload for Bedrock", slog.Error(err))
return
}
i.reqPayload = updated
if !bedrockModelSupportsAdaptiveThinking(model) {
updated, err = i.reqPayload.convertAdaptiveThinkingForBedrock()
if err != nil {
i.logger.Warn(context.Background(), "failed to convert adaptive thinking for Bedrock", slog.Error(err))
return
}
i.reqPayload = updated
}
// Filter Anthropic-Beta header to only include Bedrock-supported flags
// that the current model supports.
if i.clientHeaders != nil {
filterBedrockBetaFlags(i.clientHeaders, model)
}
// Strip body fields that Bedrock does not accept.
updated, err = i.reqPayload.removeUnsupportedBedrockFields(i.clientHeaders)
if err != nil {
i.logger.Warn(context.Background(), "failed to remove unsupported fields for Bedrock", slog.Error(err))
return
}
i.reqPayload = updated
}
// bedrockModelSupportsAdaptiveThinking returns true if the given Bedrock model ID
// supports the "adaptive" thinking type natively (i.e. Claude 4.6 models).
// See https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-adaptive-thinking.html
func bedrockModelSupportsAdaptiveThinking(model string) bool {
return strings.Contains(model, "anthropic.claude-opus-4-6") ||
strings.Contains(model, "anthropic.claude-sonnet-4-6")
}
// filterBedrockBetaFlags removes unsupported beta flags from the Anthropic-Beta
// header and also removes model-gated flags the current model doesn't support.
// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages-request-response.html
func filterBedrockBetaFlags(headers http.Header, model string) {
// Collect all flags regardless of whether the client sent them as a single
// comma-separated value (eg. Claude Code sends them in that format)
// or as multiple separate header lines.
// https://httpwg.org/specs/rfc9110.html#rfc.section.5.3
var flags []string
for _, v := range headers.Values("Anthropic-Beta") {
flags = append(flags, strings.Split(v, ",")...)
}
if len(flags) == 0 {
return
}
var keep []string
for _, flag := range flags {
trimmed := strings.TrimSpace(flag)
if !bedrockSupportedBetaFlags[trimmed] {
continue
}
// effort is only supported in Opus 4.5 on Bedrock.
if trimmed == "effort-2025-11-24" && !strings.Contains(model, "anthropic.claude-opus-4-5") {
continue
}
// context_management is only supported in Sonnet 4.5 and Haiku 4.5 models on Bedrock.
if trimmed == "context-management-2025-06-27" &&
!strings.Contains(model, "anthropic.claude-sonnet-4-5") &&
!strings.Contains(model, "anthropic.claude-haiku-4-5") {
continue
}
keep = append(keep, trimmed)
}
headers.Del("Anthropic-Beta")
for _, flag := range keep {
headers.Add("Anthropic-Beta", flag)
}
}
// writeUpstreamError marshals and writes a given error.
func (i *interceptionBase) writeUpstreamError(w http.ResponseWriter, antErr *responseError) {
if antErr == nil {
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(antErr.StatusCode)
out, err := json.Marshal(antErr)
if err != nil {
i.logger.Warn(context.Background(), "failed to marshal upstream error", slog.Error(err), slog.F("error_payload", fmt.Sprintf("%+v", antErr)))
// Response has to match expected format.
// See https://docs.claude.com/en/api/errors#error-shapes.
_, _ = w.Write([]byte(fmt.Sprintf(`{
"type":"error",
"error": {
"type": "error",
"message":"error marshaling upstream error"
},
"request_id": "%s"
}`, i.ID().String())))
} else {
_, _ = w.Write(out)
}
}
func (i *interceptionBase) hasInjectableTools() bool {
return i.mcpProxy != nil && len(i.mcpProxy.ListTools()) > 0
}
// accumulateUsage accumulates usage statistics from source into dest.
// It handles both [anthropic.Usage] and [anthropic.MessageDeltaUsage] types through [any].
// The function uses reflection to handle the differences between the types:
// - [anthropic.Usage] has CacheCreation field with ephemeral tokens
// - [anthropic.MessageDeltaUsage] doesn't have CacheCreation field
func accumulateUsage(dest, src any) {
switch d := dest.(type) {
case *anthropic.Usage:
if d == nil {
return
}
switch s := src.(type) {
case anthropic.Usage:
// Usage -> Usage
d.CacheCreation.Ephemeral1hInputTokens += s.CacheCreation.Ephemeral1hInputTokens
d.CacheCreation.Ephemeral5mInputTokens += s.CacheCreation.Ephemeral5mInputTokens
d.CacheCreationInputTokens += s.CacheCreationInputTokens
d.CacheReadInputTokens += s.CacheReadInputTokens
d.InputTokens += s.InputTokens
d.OutputTokens += s.OutputTokens
d.ServerToolUse.WebSearchRequests += s.ServerToolUse.WebSearchRequests
case anthropic.MessageDeltaUsage:
// MessageDeltaUsage -> Usage
d.CacheCreationInputTokens += s.CacheCreationInputTokens
d.CacheReadInputTokens += s.CacheReadInputTokens
d.InputTokens += s.InputTokens
d.OutputTokens += s.OutputTokens
d.ServerToolUse.WebSearchRequests += s.ServerToolUse.WebSearchRequests
}
case *anthropic.MessageDeltaUsage:
if d == nil {
return
}
switch s := src.(type) {
case anthropic.Usage:
// Usage -> MessageDeltaUsage (only common fields)
d.CacheCreationInputTokens += s.CacheCreationInputTokens
d.CacheReadInputTokens += s.CacheReadInputTokens
d.InputTokens += s.InputTokens
d.OutputTokens += s.OutputTokens
d.ServerToolUse.WebSearchRequests += s.ServerToolUse.WebSearchRequests
case anthropic.MessageDeltaUsage:
// MessageDeltaUsage -> MessageDeltaUsage
d.CacheCreationInputTokens += s.CacheCreationInputTokens
d.CacheReadInputTokens += s.CacheReadInputTokens
d.InputTokens += s.InputTokens
d.OutputTokens += s.OutputTokens
d.ServerToolUse.WebSearchRequests += s.ServerToolUse.WebSearchRequests
}
}
}
func getErrorResponse(err error) *responseError {
var apierr *anthropic.Error
if !errors.As(err, &apierr) {
return nil
}
msg := apierr.Error()
typ := string(constant.ValueOf[constant.APIError]())
var detail *anthropic.APIErrorObject
if field, ok := apierr.JSON.ExtraFields["error"]; ok {
_ = json.Unmarshal([]byte(field.Raw()), &detail)
}
if detail != nil {
msg = detail.Message
typ = string(detail.Type)
}
return &responseError{
ErrorResponse: &anthropic.ErrorResponse{
Error: anthropic.ErrorObjectUnion{
Message: msg,
Type: typ,
},
Type: constant.ValueOf[constant.Error](),
},
StatusCode: apierr.StatusCode,
}
}
var _ error = &responseError{}
type responseError struct {
*anthropic.ErrorResponse
StatusCode int `json:"-"`
}
func newErrorResponse(msg error) *responseError {
return &responseError{
ErrorResponse: &shared.ErrorResponse{
Error: shared.ErrorObjectUnion{
Message: msg.Error(),
Type: "error",
},
},
}
}
func (a *responseError) Error() string {
if a.ErrorResponse == nil {
return ""
}
return a.ErrorResponse.Error.Message
}
+993
View File
@@ -0,0 +1,993 @@
package messages //nolint:testpackage // tests unexported internals
import (
"context"
"net/http"
"testing"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/shared/constant"
mcpgo "github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/config"
"github.com/coder/coder/v2/aibridge/mcp"
"github.com/coder/coder/v2/aibridge/utils"
)
func TestScanForCorrelatingToolCallID(t *testing.T) {
t.Parallel()
tests := []struct {
name string
requestBody string
expected *string
}{
{
name: "no messages field",
requestBody: `{}`,
expected: nil,
},
{
name: "messages string",
requestBody: `{"messages":"test"}`,
expected: nil,
},
{
name: "empty messages array",
requestBody: `{"messages":[]}`,
expected: nil,
},
{
name: "last message has no tool result blocks",
requestBody: `{"messages":[{"role":"user","content":"hello"}]}`,
expected: nil,
},
{
name: "single tool result block",
requestBody: `{"messages":[{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_abc","content":"result"}]}]}`,
expected: utils.PtrTo("toolu_abc"),
},
{
name: "multiple tool result blocks returns last",
requestBody: `{"messages":[{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_first","content":"first"},{"type":"text","text":"ignored"},{"type":"tool_result","tool_use_id":"toolu_second","content":"second"}]}]}`,
expected: utils.PtrTo("toolu_second"),
},
{
name: "last message is not a tool result",
requestBody: `{"messages":[{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_first","content":"first"}]},{"role":"user","content":"some text"}]}`,
expected: nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
base := &interceptionBase{
reqPayload: mustMessagesPayload(t, tc.requestBody),
}
require.Equal(t, tc.expected, base.CorrelatingToolCallID())
})
}
}
func TestAWSBedrockValidation(t *testing.T) {
t.Parallel()
tests := []struct {
name string
cfg *config.AWSBedrock
expectError bool
errorMsg string
}{
// Valid cases: static credentials.
{
name: "static credentials with region",
cfg: &config.AWSBedrock{
Region: "us-east-1",
AccessKey: "test-key",
AccessKeySecret: "test-secret",
Model: "test-model",
SmallFastModel: "test-small-model",
},
},
{
name: "static credentials with base url",
cfg: &config.AWSBedrock{
BaseURL: "http://bedrock.internal",
AccessKey: "test-key",
AccessKeySecret: "test-secret",
Model: "test-model",
SmallFastModel: "test-small-model",
},
},
{
// There unfortunately isn't a way for us to determine precedence in a unit test,
// since the produced options take a `requestconfig.RequestConfig` input value
// which is internal to the anthropic SDK.
//
// See TestAWSBedrockIntegration which validates this.
name: "static credentials with base url & region",
cfg: &config.AWSBedrock{
Region: "us-east-1",
AccessKey: "test-key",
AccessKeySecret: "test-secret",
Model: "test-model",
SmallFastModel: "test-small-model",
},
},
// Invalid cases.
{
name: "missing region & base url",
cfg: &config.AWSBedrock{
Region: "",
AccessKey: "test-key",
AccessKeySecret: "test-secret",
Model: "test-model",
SmallFastModel: "test-small-model",
},
expectError: true,
errorMsg: "region or base url required",
},
{
name: "missing access key",
cfg: &config.AWSBedrock{
Region: "us-east-1",
AccessKeySecret: "test-secret",
Model: "test-model",
SmallFastModel: "test-small-model",
},
expectError: true,
errorMsg: "both access key and access key secret must be provided together",
},
{
name: "missing access key secret",
cfg: &config.AWSBedrock{
Region: "us-east-1",
AccessKey: "test-key",
AccessKeySecret: "",
Model: "test-model",
SmallFastModel: "test-small-model",
},
expectError: true,
errorMsg: "both access key and access key secret must be provided together",
},
{
name: "missing model",
cfg: &config.AWSBedrock{
Region: "us-east-1",
AccessKey: "test-key",
AccessKeySecret: "test-secret",
Model: "",
SmallFastModel: "test-small-model",
},
expectError: true,
errorMsg: "model required",
},
{
name: "missing small fast model",
cfg: &config.AWSBedrock{
Region: "us-east-1",
AccessKey: "test-key",
AccessKeySecret: "test-secret",
Model: "test-model",
SmallFastModel: "",
},
expectError: true,
errorMsg: "small fast model required",
},
{
name: "all fields empty",
cfg: &config.AWSBedrock{},
expectError: true,
errorMsg: "region or base url required",
},
{
name: "nil config",
cfg: nil,
expectError: true,
errorMsg: "nil config given",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
base := &interceptionBase{}
opts, err := base.withAWSBedrockOptions(context.Background(), tt.cfg)
if tt.expectError {
require.Error(t, err)
require.Contains(t, err.Error(), tt.errorMsg)
} else {
require.NotEmpty(t, opts)
require.NoError(t, err)
}
})
}
}
// TestAWSBedrockCredentialChain tests credential resolution via the AWS SDK default credential chain.
// NOTE: Cannot use t.Parallel() here because subtests use t.Setenv which requires sequential execution.
func TestAWSBedrockCredentialChain(t *testing.T) {
tests := []struct {
name string
cfg *config.AWSBedrock
envVars map[string]string
expectError bool
errorMsg string
}{
{
name: "temporary credentials via env",
cfg: &config.AWSBedrock{
Region: "us-east-1",
Model: "test-model",
SmallFastModel: "test-small-model",
},
envVars: map[string]string{
"AWS_ACCESS_KEY_ID": "test-key",
"AWS_SECRET_ACCESS_KEY": "test-secret",
},
},
{
name: "temporary credentials with session token via env",
cfg: &config.AWSBedrock{
Region: "us-east-1",
Model: "test-model",
SmallFastModel: "test-small-model",
},
envVars: map[string]string{
"AWS_ACCESS_KEY_ID": "test-key",
"AWS_SECRET_ACCESS_KEY": "test-secret",
"AWS_SESSION_TOKEN": "test-session-token",
},
},
{
// When static credentials are not provided and no environment credentials are set,
// the SDK default credential chain fails to resolve credentials.
name: "error when no credential source is configured",
cfg: &config.AWSBedrock{
Region: "us-east-1",
Model: "test-model",
SmallFastModel: "test-small-model",
},
envVars: map[string]string{
"AWS_ACCESS_KEY_ID": "",
"AWS_SECRET_ACCESS_KEY": "",
"AWS_SESSION_TOKEN": "",
"AWS_PROFILE": "",
"AWS_SHARED_CREDENTIALS_FILE": "/dev/null",
"AWS_CONFIG_FILE": "/dev/null",
"AWS_WEB_IDENTITY_TOKEN_FILE": "",
"AWS_ROLE_ARN": "",
"AWS_ROLE_SESSION_NAME": "",
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "",
"AWS_CONTAINER_CREDENTIALS_FULL_URI": "",
"AWS_CONTAINER_AUTHORIZATION_TOKEN": "",
"AWS_EC2_METADATA_DISABLED": "true",
},
expectError: true,
errorMsg: "no AWS credentials found",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for key, val := range tt.envVars {
t.Setenv(key, val)
}
base := &interceptionBase{}
opts, err := base.withAWSBedrockOptions(context.Background(), tt.cfg)
if tt.expectError {
require.Error(t, err)
require.Contains(t, err.Error(), tt.errorMsg)
} else {
require.NotEmpty(t, opts)
require.NoError(t, err)
}
})
}
}
func TestAccumulateUsage(t *testing.T) {
t.Parallel()
t.Run("Usage to Usage", func(t *testing.T) {
t.Parallel()
dest := &anthropic.Usage{
InputTokens: 10,
OutputTokens: 20,
CacheCreationInputTokens: 5,
CacheReadInputTokens: 3,
CacheCreation: anthropic.CacheCreation{
Ephemeral1hInputTokens: 2,
Ephemeral5mInputTokens: 1,
},
ServerToolUse: anthropic.ServerToolUsage{
WebSearchRequests: 1,
},
}
source := anthropic.Usage{
InputTokens: 15,
OutputTokens: 25,
CacheCreationInputTokens: 8,
CacheReadInputTokens: 4,
CacheCreation: anthropic.CacheCreation{
Ephemeral1hInputTokens: 3,
Ephemeral5mInputTokens: 2,
},
ServerToolUse: anthropic.ServerToolUsage{
WebSearchRequests: 2,
},
}
accumulateUsage(dest, source)
require.EqualValues(t, 25, dest.InputTokens)
require.EqualValues(t, 45, dest.OutputTokens)
require.EqualValues(t, 13, dest.CacheCreationInputTokens)
require.EqualValues(t, 7, dest.CacheReadInputTokens)
require.EqualValues(t, 5, dest.CacheCreation.Ephemeral1hInputTokens)
require.EqualValues(t, 3, dest.CacheCreation.Ephemeral5mInputTokens)
require.EqualValues(t, 3, dest.ServerToolUse.WebSearchRequests)
})
t.Run("MessageDeltaUsage to MessageDeltaUsage", func(t *testing.T) {
t.Parallel()
dest := &anthropic.MessageDeltaUsage{
InputTokens: 10,
OutputTokens: 20,
CacheCreationInputTokens: 5,
CacheReadInputTokens: 3,
ServerToolUse: anthropic.ServerToolUsage{
WebSearchRequests: 1,
},
}
source := anthropic.MessageDeltaUsage{
InputTokens: 15,
OutputTokens: 25,
CacheCreationInputTokens: 8,
CacheReadInputTokens: 4,
ServerToolUse: anthropic.ServerToolUsage{
WebSearchRequests: 2,
},
}
accumulateUsage(dest, source)
require.EqualValues(t, 25, dest.InputTokens)
require.EqualValues(t, 45, dest.OutputTokens)
require.EqualValues(t, 13, dest.CacheCreationInputTokens)
require.EqualValues(t, 7, dest.CacheReadInputTokens)
require.EqualValues(t, 3, dest.ServerToolUse.WebSearchRequests)
})
t.Run("Usage to MessageDeltaUsage", func(t *testing.T) {
t.Parallel()
dest := &anthropic.MessageDeltaUsage{
InputTokens: 10,
OutputTokens: 20,
CacheCreationInputTokens: 5,
CacheReadInputTokens: 3,
ServerToolUse: anthropic.ServerToolUsage{
WebSearchRequests: 1,
},
}
source := anthropic.Usage{
InputTokens: 15,
OutputTokens: 25,
CacheCreationInputTokens: 8,
CacheReadInputTokens: 4,
CacheCreation: anthropic.CacheCreation{
Ephemeral1hInputTokens: 3, // These won't be accumulated to MessageDeltaUsage
Ephemeral5mInputTokens: 2,
},
ServerToolUse: anthropic.ServerToolUsage{
WebSearchRequests: 2,
},
}
accumulateUsage(dest, source)
require.EqualValues(t, 25, dest.InputTokens)
require.EqualValues(t, 45, dest.OutputTokens)
require.EqualValues(t, 13, dest.CacheCreationInputTokens)
require.EqualValues(t, 7, dest.CacheReadInputTokens)
require.EqualValues(t, 3, dest.ServerToolUse.WebSearchRequests)
})
t.Run("MessageDeltaUsage to Usage", func(t *testing.T) {
t.Parallel()
dest := &anthropic.Usage{
InputTokens: 10,
OutputTokens: 20,
CacheCreationInputTokens: 5,
CacheReadInputTokens: 3,
CacheCreation: anthropic.CacheCreation{
Ephemeral1hInputTokens: 2,
Ephemeral5mInputTokens: 1,
},
ServerToolUse: anthropic.ServerToolUsage{
WebSearchRequests: 1,
},
}
source := anthropic.MessageDeltaUsage{
InputTokens: 15,
OutputTokens: 25,
CacheCreationInputTokens: 8,
CacheReadInputTokens: 4,
ServerToolUse: anthropic.ServerToolUsage{
WebSearchRequests: 2,
},
}
accumulateUsage(dest, source)
require.EqualValues(t, 25, dest.InputTokens)
require.EqualValues(t, 45, dest.OutputTokens)
require.EqualValues(t, 13, dest.CacheCreationInputTokens)
require.EqualValues(t, 7, dest.CacheReadInputTokens)
// Ephemeral tokens remain unchanged since MessageDeltaUsage doesn't have them
require.EqualValues(t, 2, dest.CacheCreation.Ephemeral1hInputTokens)
require.EqualValues(t, 1, dest.CacheCreation.Ephemeral5mInputTokens)
require.EqualValues(t, 3, dest.ServerToolUse.WebSearchRequests)
})
t.Run("Nil or unsupported types", func(t *testing.T) {
t.Parallel()
// Test with nil dest
var nilUsage *anthropic.Usage
source := anthropic.Usage{InputTokens: 10}
accumulateUsage(nilUsage, source) // Should not panic
// Test with unsupported types
var unsupported string
accumulateUsage(&unsupported, source) // Should not panic, just do nothing
})
}
func TestInjectTools_CacheBreakpoints(t *testing.T) {
t.Parallel()
t.Run("cache control preserved when no tools to inject", func(t *testing.T) {
t.Parallel()
// Request has existing tool with cache control, but no tools to inject.
i := &interceptionBase{
reqPayload: mustMessagesPayload(t, `{"tools":[`+
`{"name":"existing_tool","type":"custom","input_schema":{"type":"object","properties":{}},"cache_control":{"type":"ephemeral"}}]}`),
mcpProxy: &mockServerProxier{tools: nil},
logger: slog.Make(),
}
i.injectTools()
// Cache control should remain untouched since no tools were injected.
toolItems := gjson.GetBytes(i.reqPayload, "tools").Array()
require.Len(t, toolItems, 1)
require.Equal(t, "existing_tool", toolItems[0].Get("name").String())
require.Equal(t, string(constant.ValueOf[constant.Ephemeral]()), toolItems[0].Get("cache_control.type").String())
})
t.Run("cache control breakpoint is preserved by prepending injected tools", func(t *testing.T) {
t.Parallel()
// Request has existing tool with cache control.
i := &interceptionBase{
reqPayload: mustMessagesPayload(t, `{"tools":[`+
`{"name":"existing_tool","type":"custom","input_schema":{"type":"object","properties":{}},"cache_control":{"type":"ephemeral"}}]}`),
mcpProxy: &mockServerProxier{
tools: []*mcp.Tool{
{ID: "injected_tool", Name: "injected", Description: "Injected tool"},
},
},
logger: slog.Make(),
}
i.injectTools()
toolItems := gjson.GetBytes(i.reqPayload, "tools").Array()
require.Len(t, toolItems, 2)
// Injected tools are prepended.
require.Equal(t, "injected_tool", toolItems[0].Get("name").String())
require.Empty(t, toolItems[0].Get("cache_control.type").String())
// Original tool's cache control should be preserved at the end.
require.Equal(t, "existing_tool", toolItems[1].Get("name").String())
require.Equal(t, string(constant.ValueOf[constant.Ephemeral]()), toolItems[1].Get("cache_control.type").String())
})
// The cache breakpoint SHOULD be on the final tool, but may not be; we must preserve that intention.
t.Run("cache control breakpoint in non-standard location is preserved", func(t *testing.T) {
t.Parallel()
// Request has multiple tools with cache control breakpoints.
i := &interceptionBase{
reqPayload: mustMessagesPayload(t, `{"tools":[`+
`{"name":"tool_with_cache_1","type":"custom","input_schema":{"type":"object","properties":{}},"cache_control":{"type":"ephemeral"}},`+
`{"name":"tool_with_cache_2","type":"custom","input_schema":{"type":"object","properties":{}}}]}`),
mcpProxy: &mockServerProxier{
tools: []*mcp.Tool{
{ID: "injected_tool", Name: "injected", Description: "Injected tool"},
},
},
logger: slog.Make(),
}
i.injectTools()
toolItems := gjson.GetBytes(i.reqPayload, "tools").Array()
require.Len(t, toolItems, 3)
// Injected tool is prepended without cache control.
require.Equal(t, "injected_tool", toolItems[0].Get("name").String())
require.Empty(t, toolItems[0].Get("cache_control.type").String())
// Both original tools' cache controls should remain.
require.Equal(t, "tool_with_cache_1", toolItems[1].Get("name").String())
require.Equal(t, string(constant.ValueOf[constant.Ephemeral]()), toolItems[1].Get("cache_control.type").String())
require.Equal(t, "tool_with_cache_2", toolItems[2].Get("name").String())
require.Empty(t, toolItems[2].Get("cache_control.type").String())
})
t.Run("no cache control added when none originally set", func(t *testing.T) {
t.Parallel()
// Request has tools but none with cache control.
i := &interceptionBase{
reqPayload: mustMessagesPayload(t, `{"tools":[`+
`{"name":"existing_tool_no_cache","type":"custom","input_schema":{"type":"object","properties":{}}}]}`),
mcpProxy: &mockServerProxier{
tools: []*mcp.Tool{
{ID: "injected_tool", Name: "injected", Description: "Injected tool"},
},
},
logger: slog.Make(),
}
i.injectTools()
toolItems := gjson.GetBytes(i.reqPayload, "tools").Array()
require.Len(t, toolItems, 2)
// Injected tool is prepended without cache control.
require.Equal(t, "injected_tool", toolItems[0].Get("name").String())
require.Empty(t, toolItems[0].Get("cache_control.type").String())
// Original tool remains at the end without cache control.
require.Equal(t, "existing_tool_no_cache", toolItems[1].Get("name").String())
require.Empty(t, toolItems[1].Get("cache_control.type").String())
})
}
func TestInjectTools_ParallelToolCalls(t *testing.T) {
t.Parallel()
t.Run("does not modify tool choice when no tools to inject", func(t *testing.T) {
t.Parallel()
i := &interceptionBase{
reqPayload: mustMessagesPayload(t, `{"tool_choice":{"type":"auto"}}`),
mcpProxy: &mockServerProxier{tools: nil}, // No tools to inject.
logger: slog.Make(),
}
i.injectTools()
// Tool choice should remain unchanged - DisableParallelToolUse should not be set.
toolChoice := gjson.GetBytes(i.reqPayload, "tool_choice")
require.Equal(t, string(constant.ValueOf[constant.Auto]()), toolChoice.Get("type").String())
require.False(t, toolChoice.Get("disable_parallel_tool_use").Exists())
})
t.Run("disables parallel tool use for empty tool choice (default)", func(t *testing.T) {
t.Parallel()
i := &interceptionBase{
reqPayload: mustMessagesPayload(t, `{}`),
mcpProxy: &mockServerProxier{
tools: []*mcp.Tool{{ID: "test_tool", Name: "test", Description: "Test"}},
},
logger: slog.Make(),
}
i.injectTools()
toolChoice := gjson.GetBytes(i.reqPayload, "tool_choice")
require.Equal(t, string(constant.ValueOf[constant.Auto]()), toolChoice.Get("type").String())
require.True(t, toolChoice.Get("disable_parallel_tool_use").Exists())
require.True(t, toolChoice.Get("disable_parallel_tool_use").Bool())
})
t.Run("disables parallel tool use for explicit auto tool choice", func(t *testing.T) {
t.Parallel()
i := &interceptionBase{
reqPayload: mustMessagesPayload(t, `{"tool_choice":{"type":"auto"}}`),
mcpProxy: &mockServerProxier{
tools: []*mcp.Tool{{ID: "test_tool", Name: "test", Description: "Test"}},
},
logger: slog.Make(),
}
i.injectTools()
toolChoice := gjson.GetBytes(i.reqPayload, "tool_choice")
require.Equal(t, string(constant.ValueOf[constant.Auto]()), toolChoice.Get("type").String())
require.True(t, toolChoice.Get("disable_parallel_tool_use").Exists())
require.True(t, toolChoice.Get("disable_parallel_tool_use").Bool())
})
t.Run("disables parallel tool use for any tool choice", func(t *testing.T) {
t.Parallel()
i := &interceptionBase{
reqPayload: mustMessagesPayload(t, `{"tool_choice":{"type":"any"}}`),
mcpProxy: &mockServerProxier{
tools: []*mcp.Tool{{ID: "test_tool", Name: "test", Description: "Test"}},
},
logger: slog.Make(),
}
i.injectTools()
toolChoice := gjson.GetBytes(i.reqPayload, "tool_choice")
require.Equal(t, string(constant.ValueOf[constant.Any]()), toolChoice.Get("type").String())
require.True(t, toolChoice.Get("disable_parallel_tool_use").Exists())
require.True(t, toolChoice.Get("disable_parallel_tool_use").Bool())
})
t.Run("disables parallel tool use for tool choice type", func(t *testing.T) {
t.Parallel()
i := &interceptionBase{
reqPayload: mustMessagesPayload(t, `{"tool_choice":{"type":"tool","name":"specific_tool"}}`),
mcpProxy: &mockServerProxier{
tools: []*mcp.Tool{{ID: "test_tool", Name: "test", Description: "Test"}},
},
logger: slog.Make(),
}
i.injectTools()
toolChoice := gjson.GetBytes(i.reqPayload, "tool_choice")
require.Equal(t, string(constant.ValueOf[constant.Tool]()), toolChoice.Get("type").String())
require.True(t, toolChoice.Get("disable_parallel_tool_use").Exists())
require.True(t, toolChoice.Get("disable_parallel_tool_use").Bool())
})
t.Run("no-op for none tool choice type", func(t *testing.T) {
t.Parallel()
i := &interceptionBase{
reqPayload: mustMessagesPayload(t, `{"tool_choice":{"type":"none"}}`),
mcpProxy: &mockServerProxier{
tools: []*mcp.Tool{{ID: "test_tool", Name: "test", Description: "Test"}},
},
logger: slog.Make(),
}
i.injectTools()
// Tools are still injected.
require.Len(t, gjson.GetBytes(i.reqPayload, "tools").Array(), 1)
// But no parallel tool use modification for "none" type.
toolChoice := gjson.GetBytes(i.reqPayload, "tool_choice")
require.Equal(t, string(constant.ValueOf[constant.None]()), toolChoice.Get("type").String())
require.False(t, toolChoice.Get("disable_parallel_tool_use").Exists())
})
}
func TestAugmentRequestForBedrock_AdaptiveThinking(t *testing.T) {
t.Parallel()
tests := []struct {
name string
bedrockModel string
requestBody string
clientBetaFlags string
expectThinkingType string
expectBudgetTokens int64 // 0 means budget_tokens should not be present
expectRemovedFields []string
expectKeptFields []string
expectBetaValues []string // expected separate Anthropic-Beta header values
}{
{
name: "non_4_6_model_with_adaptive_thinking_gets_converted",
bedrockModel: "anthropic.claude-sonnet-4-5-20250929-v1:0",
requestBody: `{"max_tokens":10000,"thinking":{"type":"adaptive"}}`,
expectThinkingType: "enabled",
expectBudgetTokens: 8000, // 10000 * 0.8 (default/high effort)
},
{
name: "non_4_6_model_with_adaptive_thinking_and_small_max_tokens_disables_thinking",
bedrockModel: "anthropic.claude-sonnet-4-5-20250929-v1:0",
requestBody: `{"max_tokens":1000,"thinking":{"type":"adaptive"}}`,
expectThinkingType: "disabled",
},
{
name: "opus_4_6_model_with_adaptive_thinking_is_not_converted",
bedrockModel: "anthropic.claude-opus-4-6-v1",
requestBody: `{"max_tokens":10000,"thinking":{"type":"adaptive"}}`,
expectThinkingType: "adaptive",
},
{
name: "sonnet_4_6_model_with_adaptive_thinking_is_not_converted",
bedrockModel: "anthropic.claude-sonnet-4-6",
requestBody: `{"max_tokens":10000,"thinking":{"type":"adaptive"}}`,
expectThinkingType: "adaptive",
},
{
name: "non_4_6_model_with_no_thinking_field_is_unchanged",
bedrockModel: "anthropic.claude-sonnet-4-5-20250929-v1:0",
requestBody: `{"max_tokens":10000}`,
},
{
name: "non_4_6_model_with_enabled_thinking_is_unchanged",
bedrockModel: "anthropic.claude-sonnet-4-5-20250929-v1:0",
requestBody: `{"max_tokens":10000,"thinking":{"type":"enabled","budget_tokens":5000}}`,
expectThinkingType: "enabled",
expectBudgetTokens: 5000,
},
{
name: "output_config_stripped_without_beta_flag_and_effort_used_for_budget",
bedrockModel: "anthropic.claude-sonnet-4-5-20250929-v1:0",
requestBody: `{"max_tokens":10000,"thinking":{"type":"adaptive"},"output_config":{"effort":"low"}}`,
expectThinkingType: "enabled",
expectBudgetTokens: 2000, // 10000 * 0.2 (low effort)
expectRemovedFields: []string{"output_config"},
},
{
name: "output_config_kept_when_effort_beta_flag_present_on_opus_4_5",
bedrockModel: "anthropic.claude-opus-4-5-20250929-v1:0",
clientBetaFlags: "effort-2025-11-24,interleaved-thinking-2025-05-14",
requestBody: `{"max_tokens":10000,"output_config":{"effort":"high"}}`,
expectKeptFields: []string{"output_config"},
expectBetaValues: []string{"effort-2025-11-24", "interleaved-thinking-2025-05-14"},
},
{
name: "output_config_stripped_for_non_opus_4_5_even_with_effort_beta_flag",
bedrockModel: "anthropic.claude-sonnet-4-5-20250929-v1:0",
clientBetaFlags: "effort-2025-11-24,interleaved-thinking-2025-05-14",
requestBody: `{"max_tokens":10000,"output_config":{"effort":"high"}}`,
expectRemovedFields: []string{"output_config"},
expectBetaValues: []string{"interleaved-thinking-2025-05-14"},
},
{
name: "context_management_kept_when_beta_flag_present",
bedrockModel: "anthropic.claude-sonnet-4-5-20250929-v1:0",
clientBetaFlags: "context-management-2025-06-27",
requestBody: `{"max_tokens":10000,"context_management":{"type":"auto"}}`,
expectKeptFields: []string{"context_management"},
expectBetaValues: []string{"context-management-2025-06-27"},
},
{
name: "context_management_stripped_without_beta_flag",
bedrockModel: "anthropic.claude-sonnet-4-5-20250929-v1:0",
requestBody: `{"max_tokens":10000,"context_management":{"type":"auto"}}`,
expectRemovedFields: []string{"context_management"},
},
{
name: "context_management_stripped_for_unsupported_model_even_with_beta_flag",
bedrockModel: "anthropic.claude-opus-4-6-v1",
clientBetaFlags: "context-management-2025-06-27",
requestBody: `{"max_tokens":10000,"thinking":{"type":"adaptive"},"context_management":{"type":"auto"}}`,
expectThinkingType: "adaptive",
expectRemovedFields: []string{"context_management"},
},
{
name: "unsupported_beta_flags_are_filtered_out",
bedrockModel: "anthropic.claude-sonnet-4-5-20250929-v1:0",
clientBetaFlags: "claude-code-20250219,interleaved-thinking-2025-05-14,prompt-caching-scope-2026-01-05",
requestBody: `{"max_tokens":10000}`,
expectBetaValues: []string{"interleaved-thinking-2025-05-14"},
},
{
name: "all_unsupported_fields_stripped_and_beta_flags_filtered",
bedrockModel: "anthropic.claude-sonnet-4-5-20250929-v1:0",
clientBetaFlags: "claude-code-20250219,prompt-caching-scope-2026-01-05",
requestBody: `{"max_tokens":10000,"output_config":{"effort":"high"},"metadata":{"user_id":"u123"},"service_tier":"auto","container":"ctr_abc","inference_geo":"us","context_management":{"type":"auto"}}`,
expectRemovedFields: []string{"output_config", "metadata", "service_tier", "container", "inference_geo", "context_management"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var clientHeaders http.Header
if tc.clientBetaFlags != "" {
clientHeaders = http.Header{
"Anthropic-Beta": {tc.clientBetaFlags},
}
}
i := &interceptionBase{
reqPayload: mustMessagesPayload(t, tc.requestBody),
bedrockCfg: &config.AWSBedrock{
Model: tc.bedrockModel,
SmallFastModel: "anthropic.claude-haiku-3-5",
},
clientHeaders: clientHeaders,
logger: slog.Make(),
}
i.augmentRequestForBedrock()
thinkingType := gjson.GetBytes(i.reqPayload, "thinking.type")
if tc.expectThinkingType == "" {
require.False(t, thinkingType.Exists())
} else {
require.Equal(t, tc.expectThinkingType, thinkingType.String())
}
budgetTokens := gjson.GetBytes(i.reqPayload, "thinking.budget_tokens")
if tc.expectBudgetTokens == 0 {
require.False(t, budgetTokens.Exists(), "budget_tokens should not be set")
} else {
require.Equal(t, tc.expectBudgetTokens, budgetTokens.Int())
}
// Model should always be set to the bedrock model.
require.Equal(t, tc.bedrockModel, gjson.GetBytes(i.reqPayload, "model").String())
// Verify expected fields are removed.
for _, field := range tc.expectRemovedFields {
require.False(t, gjson.GetBytes(i.reqPayload, field).Exists(), "%s should be removed", field)
}
// Verify expected fields are kept.
for _, field := range tc.expectKeptFields {
require.True(t, gjson.GetBytes(i.reqPayload, field).Exists(), "%s should be kept", field)
}
got := clientHeaders.Values("Anthropic-Beta")
require.Equal(t, tc.expectBetaValues, got)
})
}
}
func mustMessagesPayload(t *testing.T, requestBody string) RequestPayload {
t.Helper()
payload, err := NewRequestPayload([]byte(requestBody))
require.NoError(t, err)
return payload
}
// mockServerProxier is a test implementation of mcp.ServerProxier.
type mockServerProxier struct {
tools []*mcp.Tool
}
func (*mockServerProxier) Init(context.Context) error {
return nil
}
func (*mockServerProxier) Shutdown(context.Context) error {
return nil
}
func (m *mockServerProxier) ListTools() []*mcp.Tool {
return m.tools
}
func (m *mockServerProxier) GetTool(id string) *mcp.Tool {
for _, t := range m.tools {
if t.ID == id {
return t
}
}
return nil
}
func (*mockServerProxier) CallTool(context.Context, string, any) (*mcpgo.CallToolResult, error) {
return nil, nil //nolint:nilnil // mock: no-op implementation
}
func TestFilterBedrockBetaFlags(t *testing.T) {
t.Parallel()
tests := []struct {
name string
model string
inputValues []string // header values to set (each element is a separate header value)
expectValues []string // expected separate header values after filtering
}{
{
name: "empty header",
model: "anthropic.claude-sonnet-4-5-20250929-v1:0",
inputValues: nil,
expectValues: nil,
},
{
name: "all supported flags kept",
model: "anthropic.claude-opus-4-5-20250929-v1:0",
inputValues: []string{"interleaved-thinking-2025-05-14,effort-2025-11-24"},
expectValues: []string{"interleaved-thinking-2025-05-14", "effort-2025-11-24"},
},
{
name: "unsupported flags removed",
model: "anthropic.claude-sonnet-4-5-20250929-v1:0",
inputValues: []string{"claude-code-20250219,interleaved-thinking-2025-05-14,prompt-caching-scope-2026-01-05"},
expectValues: []string{"interleaved-thinking-2025-05-14"},
},
{
name: "header removed when all flags unsupported",
model: "anthropic.claude-sonnet-4-5-20250929-v1:0",
inputValues: []string{"claude-code-20250219,prompt-caching-scope-2026-01-05"},
expectValues: nil,
},
{
name: "effort flag removed for non opus 4.5 model",
model: "anthropic.claude-sonnet-4-5-20250929-v1:0",
inputValues: []string{"effort-2025-11-24,interleaved-thinking-2025-05-14"},
expectValues: []string{"interleaved-thinking-2025-05-14"},
},
{
name: "effort flag kept for opus 4.5 model",
model: "anthropic.claude-opus-4-5-20250929-v1:0",
inputValues: []string{"effort-2025-11-24,interleaved-thinking-2025-05-14"},
expectValues: []string{"effort-2025-11-24", "interleaved-thinking-2025-05-14"},
},
{
name: "context management kept for sonnet 4.5",
model: "anthropic.claude-sonnet-4-5-20250929-v1:0",
inputValues: []string{"context-management-2025-06-27"},
expectValues: []string{"context-management-2025-06-27"},
},
{
name: "context management kept for haiku 4.5",
model: "anthropic.claude-haiku-4-5-20250929-v1:0",
inputValues: []string{"context-management-2025-06-27"},
expectValues: []string{"context-management-2025-06-27"},
},
{
name: "context management removed for unsupported model",
model: "anthropic.claude-opus-4-6-v1",
inputValues: []string{"context-management-2025-06-27,interleaved-thinking-2025-05-14"},
expectValues: []string{"interleaved-thinking-2025-05-14"},
},
{
name: "separate header values are handled correctly",
model: "anthropic.claude-sonnet-4-5-20250929-v1:0",
inputValues: []string{"interleaved-thinking-2025-05-14", "context-management-2025-06-27"},
expectValues: []string{"interleaved-thinking-2025-05-14", "context-management-2025-06-27"},
},
{
name: "mixed comma-joined and separate header values",
model: "anthropic.claude-opus-4-5-20250929-v1:0",
inputValues: []string{"interleaved-thinking-2025-05-14,effort-2025-11-24", "token-efficient-tools-2025-02-19"},
expectValues: []string{"interleaved-thinking-2025-05-14", "effort-2025-11-24", "token-efficient-tools-2025-02-19"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
headers := http.Header{}
for _, v := range tc.inputValues {
headers.Add("Anthropic-Beta", v)
}
filterBedrockBetaFlags(headers, tc.model)
// Each kept flag should be a separate header value.
got := headers.Values("Anthropic-Beta")
require.Equal(t, tc.expectValues, got)
})
}
}
+342
View File
@@ -0,0 +1,342 @@
package messages
import (
"context"
"fmt"
"net/http"
"time"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
"github.com/google/uuid"
mcplib "github.com/mark3labs/mcp-go/mcp"
"github.com/tidwall/sjson"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/config"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/intercept"
"github.com/coder/coder/v2/aibridge/intercept/eventstream"
"github.com/coder/coder/v2/aibridge/mcp"
"github.com/coder/coder/v2/aibridge/recorder"
"github.com/coder/coder/v2/aibridge/tracing"
)
type BlockingInterception struct {
interceptionBase
}
func NewBlockingInterceptor(
id uuid.UUID,
reqPayload RequestPayload,
providerName string,
cfg config.Anthropic,
bedrockCfg *config.AWSBedrock,
clientHeaders http.Header,
authHeaderName string,
tracer trace.Tracer,
cred intercept.CredentialInfo,
) *BlockingInterception {
return &BlockingInterception{interceptionBase: interceptionBase{
id: id,
providerName: providerName,
reqPayload: reqPayload,
cfg: cfg,
bedrockCfg: bedrockCfg,
clientHeaders: clientHeaders,
authHeaderName: authHeaderName,
tracer: tracer,
credential: cred,
}}
}
func (i *BlockingInterception) Setup(logger slog.Logger, rec recorder.Recorder, mcpProxy mcp.ServerProxier) {
i.interceptionBase.Setup(logger.Named("blocking"), rec, mcpProxy)
}
func (i *BlockingInterception) TraceAttributes(r *http.Request) []attribute.KeyValue {
return i.interceptionBase.baseTraceAttributes(r, false)
}
func (*BlockingInterception) Streaming() bool {
return false
}
func (i *BlockingInterception) ProcessRequest(w http.ResponseWriter, r *http.Request) (outErr error) {
if len(i.reqPayload) == 0 {
return xerrors.New("developer error: request payload is empty")
}
ctx, span := i.tracer.Start(r.Context(), "Intercept.ProcessRequest", trace.WithAttributes(tracing.InterceptionAttributesFromContext(r.Context())...))
defer tracing.EndSpanErr(span, &outErr)
i.injectTools()
var prompt *string
promptText, promptFound, promptErr := i.reqPayload.lastUserPrompt()
if promptErr != nil {
i.logger.Warn(ctx, "failed to retrieve last user prompt", slog.Error(promptErr))
} else if promptFound {
prompt = &promptText
}
// TODO(ssncferreira): inject actor headers directly in the client-header
// middleware instead of using SDK options.
opts := []option.RequestOption{option.WithRequestTimeout(time.Second * 600)}
if actor := aibcontext.ActorFromContext(r.Context()); actor != nil && i.cfg.SendActorHeaders {
opts = append(opts, intercept.ActorHeadersAsAnthropicOpts(actor)...)
}
svc, err := i.newMessagesService(ctx, opts...)
if err != nil {
err = xerrors.Errorf("create anthropic client: %w", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return err
}
logger := i.logger.With(slog.F("model", i.Model()))
var resp *anthropic.Message
// Accumulate usage across the entire streaming interaction (including tool reinvocations).
var cumulativeUsage anthropic.Usage
for {
// TODO add outer loop span (https://github.com/coder/aibridge/issues/67)
resp, err = i.newMessage(ctx, svc)
if err != nil {
if eventstream.IsConnError(err) {
// Can't write a response, just error out.
return xerrors.Errorf("upstream connection closed: %w", err)
}
if antErr := getErrorResponse(err); antErr != nil {
i.writeUpstreamError(w, antErr)
return xerrors.Errorf("anthropic API error: %w", err)
}
http.Error(w, "internal error", http.StatusInternalServerError)
return xerrors.Errorf("internal error: %w", err)
}
if prompt != nil {
_ = i.recorder.RecordPromptUsage(ctx, &recorder.PromptUsageRecord{
InterceptionID: i.ID().String(),
MsgID: resp.ID,
Prompt: *prompt,
})
prompt = nil
}
_ = i.recorder.RecordTokenUsage(ctx, &recorder.TokenUsageRecord{
InterceptionID: i.ID().String(),
MsgID: resp.ID,
Input: resp.Usage.InputTokens,
Output: resp.Usage.OutputTokens,
CacheReadInputTokens: resp.Usage.CacheReadInputTokens,
CacheWriteInputTokens: resp.Usage.CacheCreationInputTokens,
ExtraTokenTypes: map[string]int64{
"web_search_requests": resp.Usage.ServerToolUse.WebSearchRequests,
"cache_creation_input": resp.Usage.CacheCreationInputTokens, // TODO: remove from ExtraTokenTypes (https://github.com/coder/aibridge/issues/243)
"cache_read_input": resp.Usage.CacheReadInputTokens, // TODO: remove from ExtraTokenTypes (https://github.com/coder/aibridge/issues/243)
"cache_ephemeral_1h_input": resp.Usage.CacheCreation.Ephemeral1hInputTokens,
"cache_ephemeral_5m_input": resp.Usage.CacheCreation.Ephemeral5mInputTokens,
},
})
accumulateUsage(&cumulativeUsage, resp.Usage)
// Capture any thinking blocks that were returned.
for _, t := range i.extractModelThoughts(resp) {
_ = i.recorder.RecordModelThought(ctx, &recorder.ModelThoughtRecord{
InterceptionID: i.ID().String(),
Content: t.Content,
Metadata: t.Metadata,
})
}
// Handle tool calls.
var pendingToolCalls []anthropic.ToolUseBlock
for _, c := range resp.Content {
toolUse := c.AsToolUse()
if toolUse.ID == "" {
continue
}
if i.mcpProxy != nil && i.mcpProxy.GetTool(toolUse.Name) != nil {
pendingToolCalls = append(pendingToolCalls, toolUse)
continue
}
// If tool is not injected, track it since the client will be handling it.
_ = i.recorder.RecordToolUsage(ctx, &recorder.ToolUsageRecord{
InterceptionID: i.ID().String(),
MsgID: resp.ID,
ToolCallID: toolUse.ID,
Tool: toolUse.Name,
Args: toolUse.Input,
Injected: false,
})
}
// If no injected tool calls, we're done.
if len(pendingToolCalls) == 0 {
break
}
var loopMessages []anthropic.MessageParam
loopMessages = append(loopMessages, resp.ToParam())
// Process each pending tool call.
for _, tc := range pendingToolCalls {
if i.mcpProxy == nil {
continue
}
tool := i.mcpProxy.GetTool(tc.Name)
if tool == nil {
logger.Warn(ctx, "tool not found in manager", slog.F("tool", tc.Name))
// Continue to next tool call, but still append an error tool_result
loopMessages = append(loopMessages,
anthropic.NewUserMessage(anthropic.NewToolResultBlock(tc.ID, fmt.Sprintf("Error: tool %s not found", tc.Name), true)),
)
continue
}
res, err := tool.Call(ctx, tc.Input, i.tracer)
_ = i.recorder.RecordToolUsage(ctx, &recorder.ToolUsageRecord{
InterceptionID: i.ID().String(),
MsgID: resp.ID,
ToolCallID: tc.ID,
ServerURL: &tool.ServerURL,
Tool: tool.Name,
Args: tc.Input,
Injected: true,
InvocationError: err,
})
if err != nil {
// Always provide a tool_result even if the tool call failed
loopMessages = append(loopMessages,
anthropic.NewUserMessage(anthropic.NewToolResultBlock(tc.ID, fmt.Sprintf("Error: calling tool: %v", err), true)),
)
continue
}
// Process tool result
toolResult := anthropic.ContentBlockParamUnion{
OfToolResult: &anthropic.ToolResultBlockParam{
ToolUseID: tc.ID,
IsError: anthropic.Bool(false),
},
}
var hasValidResult bool
for _, content := range res.Content {
switch cb := content.(type) {
case mcplib.TextContent:
toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.ToolResultBlockParamContentUnion{
OfText: &anthropic.TextBlockParam{
Text: cb.Text,
},
})
hasValidResult = true
// TODO: is there a more correct way of handling these non-text content responses?
case mcplib.EmbeddedResource:
switch resource := cb.Resource.(type) {
case mcplib.TextResourceContents:
val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s",
resource.MIMEType, resource.URI, resource.Text)
toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.ToolResultBlockParamContentUnion{
OfText: &anthropic.TextBlockParam{
Text: val,
},
})
hasValidResult = true
case mcplib.BlobResourceContents:
val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s",
resource.MIMEType, resource.URI, resource.Blob)
toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.ToolResultBlockParamContentUnion{
OfText: &anthropic.TextBlockParam{
Text: val,
},
})
hasValidResult = true
default:
i.logger.Warn(ctx, "unknown embedded resource type", slog.F("type", fmt.Sprintf("%T", resource)))
toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.ToolResultBlockParamContentUnion{
OfText: &anthropic.TextBlockParam{
Text: "Error: unknown embedded resource type",
},
})
toolResult.OfToolResult.IsError = anthropic.Bool(true)
hasValidResult = true
}
default:
i.logger.Warn(ctx, "not handling non-text tool result", slog.F("type", fmt.Sprintf("%T", cb)))
toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.ToolResultBlockParamContentUnion{
OfText: &anthropic.TextBlockParam{
Text: "Error: unsupported tool result type",
},
})
toolResult.OfToolResult.IsError = anthropic.Bool(true)
hasValidResult = true
}
}
// If no content was processed, still add a tool_result
if !hasValidResult {
i.logger.Warn(ctx, "no tool result added", slog.F("content_len", len(res.Content)), slog.F("is_error", res.IsError))
toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.ToolResultBlockParamContentUnion{
OfText: &anthropic.TextBlockParam{
Text: "Error: no valid tool result content",
},
})
toolResult.OfToolResult.IsError = anthropic.Bool(true)
}
if len(toolResult.OfToolResult.Content) > 0 {
loopMessages = append(loopMessages, anthropic.NewUserMessage(toolResult))
}
}
updatedPayload, rewriteErr := i.reqPayload.appendedMessages(loopMessages)
if rewriteErr != nil {
http.Error(w, rewriteErr.Error(), http.StatusInternalServerError)
return xerrors.Errorf("rewrite payload for agentic loop: %w", rewriteErr)
}
i.reqPayload = updatedPayload
}
if resp == nil {
return nil
}
// Overwrite response identifier since proxy obscures injected tool call invocations.
sj, err := sjson.Set(resp.RawJSON(), "id", i.ID().String())
if err != nil {
return xerrors.Errorf("marshal response id failed: %w", err)
}
// Overwrite the response's usage with the cumulative usage across any inner loops which invokes injected MCP tools.
sj, err = sjson.Set(sj, "usage", cumulativeUsage)
if err != nil {
return xerrors.Errorf("marshal response usage failed: %w", err)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(sj))
return nil
}
func (i *BlockingInterception) newMessage(ctx context.Context, svc anthropic.MessageService) (_ *anthropic.Message, outErr error) {
ctx, span := i.tracer.Start(ctx, "Intercept.ProcessRequest.Upstream", trace.WithAttributes(tracing.InterceptionAttributesFromContext(ctx)...))
defer tracing.EndSpanErr(span, &outErr)
return svc.New(ctx, anthropic.MessageNewParams{}, i.withBody())
}
+412
View File
@@ -0,0 +1,412 @@
package messages
import (
"bytes"
"encoding/json"
"net/http"
"slices"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/shared/constant"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"golang.org/x/xerrors"
)
const (
// Absolute JSON paths from the request root.
messagesReqPathMessages = "messages"
messagesReqPathMaxTokens = "max_tokens"
messagesReqPathModel = "model"
messagesReqPathOutputConfig = "output_config"
messagesReqPathOutputConfigEffort = "output_config.effort"
messagesReqPathMetadata = "metadata"
messagesReqPathServiceTier = "service_tier"
messagesReqPathContainer = "container"
messagesReqPathInferenceGeo = "inference_geo"
messagesReqPathContextManagement = "context_management"
messagesReqPathStream = "stream"
messagesReqPathThinking = "thinking"
messagesReqPathThinkingBudgetTokens = "thinking.budget_tokens"
messagesReqPathThinkingType = "thinking.type"
messagesReqPathToolChoice = "tool_choice"
messagesReqPathToolChoiceDisableParallel = "tool_choice.disable_parallel_tool_use"
messagesReqPathToolChoiceType = "tool_choice.type"
messagesReqPathTools = "tools"
// Relative field names used within sub-objects.
messagesReqFieldContent = "content"
messagesReqFieldRole = "role"
messagesReqFieldText = "text"
messagesReqFieldToolUseID = "tool_use_id"
messagesReqFieldType = "type"
)
const (
constAdaptive = "adaptive"
constDisabled = "disabled"
constEnabled = "enabled"
)
var (
constAny = string(constant.ValueOf[constant.Any]())
constAuto = string(constant.ValueOf[constant.Auto]())
constNone = string(constant.ValueOf[constant.None]())
constText = string(constant.ValueOf[constant.Text]())
constTool = string(constant.ValueOf[constant.Tool]())
constToolResult = string(constant.ValueOf[constant.ToolResult]())
constUser = string(anthropic.MessageParamRoleUser)
// bedrockUnsupportedFields are top-level fields present in the Anthropic Messages
// API that are absent from the Bedrock request body schema. Sending them results
// in a 400 "Extra inputs are not permitted" error.
//
// Anthropic API fields: https://platform.claude.com/docs/en/api/messages/create
// Bedrock request body: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages-request-response.html
bedrockUnsupportedFields = []string{
messagesReqPathMetadata,
messagesReqPathServiceTier,
messagesReqPathContainer,
messagesReqPathInferenceGeo,
}
// bedrockBetaGatedFields maps body fields to the beta flag that enables them.
// If the beta flag is present in the (already-filtered) Anthropic-Beta header,
// the field is kept; otherwise it is stripped. Model-specific beta flags must
// be removed from the header before this check (see filterBedrockBetaFlags).
bedrockBetaGatedFields = map[string]string{
// output_config requires the effort beta (Opus 4.5 only).
messagesReqPathOutputConfig: "effort-2025-11-24",
// context_management requires the context-management beta (Sonnet 4.5, Haiku 4.5).
messagesReqPathContextManagement: "context-management-2025-06-27",
}
)
// RequestPayload is raw JSON bytes of an Anthropic Messages API request.
// Methods provide package-specific reads and rewrites while preserving the
// original body for upstream pass-through.
type RequestPayload []byte
func NewRequestPayload(raw []byte) (RequestPayload, error) {
if len(bytes.TrimSpace(raw)) == 0 {
return nil, xerrors.New("messages empty request body")
}
if !json.Valid(raw) {
return nil, xerrors.New("messages invalid JSON request body")
}
return RequestPayload(raw), nil
}
func (p RequestPayload) Stream() bool {
v := gjson.GetBytes(p, messagesReqPathStream)
if !v.IsBool() {
return false
}
return v.Bool()
}
func (p RequestPayload) model() string {
return gjson.GetBytes(p, messagesReqPathModel).Str
}
func (p RequestPayload) correlatingToolCallID() *string {
messages := gjson.GetBytes(p, messagesReqPathMessages)
if !messages.IsArray() {
return nil
}
messageItems := messages.Array()
if len(messageItems) == 0 {
return nil
}
content := messageItems[len(messageItems)-1].Get(messagesReqFieldContent)
if !content.IsArray() {
return nil
}
contentItems := content.Array()
for idx := len(contentItems) - 1; idx >= 0; idx-- {
contentItem := contentItems[idx]
if contentItem.Get(messagesReqFieldType).String() != constToolResult {
continue
}
toolUseID := contentItem.Get(messagesReqFieldToolUseID).String()
if toolUseID == "" {
continue
}
return &toolUseID
}
return nil
}
// lastUserPrompt returns the prompt text from the last user message. If no prompt
// is found, it returns empty string, false, nil. Unexpected shapes are treated as
// unsupported and do not fail the request path.
func (p RequestPayload) lastUserPrompt() (string, bool, error) {
messages := gjson.GetBytes(p, messagesReqPathMessages)
if !messages.Exists() || messages.Type == gjson.Null {
return "", false, nil
}
if !messages.IsArray() {
return "", false, xerrors.Errorf("unexpected messages type: %s", messages.Type)
}
messageItems := messages.Array()
if len(messageItems) == 0 {
return "", false, nil
}
lastMessage := messageItems[len(messageItems)-1]
if lastMessage.Get(messagesReqFieldRole).String() != constUser {
return "", false, nil
}
content := lastMessage.Get(messagesReqFieldContent)
if !content.Exists() || content.Type == gjson.Null {
return "", false, nil
}
if content.Type == gjson.String {
return content.String(), true, nil
}
if !content.IsArray() {
return "", false, xerrors.Errorf("unexpected message content type: %s", content.Type)
}
contentItems := content.Array()
for idx := len(contentItems) - 1; idx >= 0; idx-- {
contentItem := contentItems[idx]
if contentItem.Get(messagesReqFieldType).String() != constText {
continue
}
text := contentItem.Get(messagesReqFieldText)
if text.Type != gjson.String {
continue
}
return text.String(), true, nil
}
return "", false, nil
}
func (p RequestPayload) injectTools(injected []anthropic.ToolUnionParam) (RequestPayload, error) {
if len(injected) == 0 {
return p, nil
}
existing, err := p.tools()
if err != nil {
return p, xerrors.Errorf("get existing tools: %w", err)
}
// Using []json.Marshaler to merge differently-typed slices ([]anthropic.ToolUnionParam
// and []json.Marshaler containing json.RawMessage) keeps JSON re-marshalings to a minimum:
// sjson.SetBytes marshals each element exactly once, and json.RawMessage
// elements are passed through without re-serialization.
allTools := make([]json.Marshaler, 0, len(injected)+len(existing))
for _, tool := range injected {
allTools = append(allTools, tool)
}
for _, e := range existing {
allTools = append(allTools, e)
}
return p.set(messagesReqPathTools, allTools)
}
func (p RequestPayload) disableParallelToolCalls() (RequestPayload, error) {
toolChoice := gjson.GetBytes(p, messagesReqPathToolChoice)
// If no tool_choice was defined, assume auto.
// See https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use#parallel-tool-use.
if !toolChoice.Exists() || toolChoice.Type == gjson.Null {
updated, err := p.set(messagesReqPathToolChoiceType, constAuto)
if err != nil {
return p, xerrors.Errorf("set tool choice type: %w", err)
}
return updated.set(messagesReqPathToolChoiceDisableParallel, true)
}
if !toolChoice.IsObject() {
return p, xerrors.Errorf("unsupported tool_choice type: %s", toolChoice.Type)
}
toolChoiceType := gjson.GetBytes(p, messagesReqPathToolChoiceType)
if toolChoiceType.Exists() && toolChoiceType.Type != gjson.String {
return p, xerrors.Errorf("unsupported tool_choice.type type: %s", toolChoiceType.Type)
}
switch toolChoiceType.String() {
case "":
updated, err := p.set(messagesReqPathToolChoiceType, constAuto)
if err != nil {
return p, xerrors.Errorf("set tool_choice.type: %w", err)
}
return updated.set(messagesReqPathToolChoiceDisableParallel, true)
case constAuto, constAny, constTool:
return p.set(messagesReqPathToolChoiceDisableParallel, true)
case constNone:
return p, nil
default:
return p, xerrors.Errorf("unsupported tool_choice.type value: %q", toolChoiceType.String())
}
}
func (p RequestPayload) appendedMessages(newMessages []anthropic.MessageParam) (RequestPayload, error) {
if len(newMessages) == 0 {
return p, nil
}
existing, err := p.messages()
if err != nil {
return p, xerrors.Errorf("get existing messages: %w", err)
}
// Using []json.Marshaler to merge differently-typed slices ([]json.Marshaler containing
// json.RawMessage and []anthropic.MessageParam) keeps JSON re-marshalings
// to a minimum: sjson.SetBytes marshals each element exactly once, and
// json.RawMessage elements are passed through without re-serialization.
allMessages := make([]json.Marshaler, 0, len(existing)+len(newMessages))
for _, e := range existing {
allMessages = append(allMessages, e)
}
for _, new := range newMessages {
allMessages = append(allMessages, new)
}
return p.set(messagesReqPathMessages, allMessages)
}
func (p RequestPayload) withModel(model string) (RequestPayload, error) {
return p.set(messagesReqPathModel, model)
}
func (p RequestPayload) messages() ([]json.RawMessage, error) {
messages := gjson.GetBytes(p, messagesReqPathMessages)
if !messages.Exists() || messages.Type == gjson.Null {
return nil, nil
}
if !messages.IsArray() {
return nil, xerrors.Errorf("unsupported messages type: %s", messages.Type)
}
return p.resultToRawMessage(messages.Array()), nil
}
func (p RequestPayload) tools() ([]json.RawMessage, error) {
tools := gjson.GetBytes(p, messagesReqPathTools)
if !tools.Exists() || tools.Type == gjson.Null {
return nil, nil
}
if !tools.IsArray() {
return nil, xerrors.Errorf("unsupported tools type: %s", tools.Type)
}
return p.resultToRawMessage(tools.Array()), nil
}
func (RequestPayload) resultToRawMessage(items []gjson.Result) []json.RawMessage {
// gjson.Result conversion to json.RawMessage is needed because
// gjson.Result does not implement json.Marshaler. It would
// serialize its struct fields instead of the raw JSON it represents.
rawMessages := make([]json.RawMessage, 0, len(items))
for _, item := range items {
rawMessages = append(rawMessages, json.RawMessage(item.Raw))
}
return rawMessages
}
// convertAdaptiveThinkingForBedrock converts thinking.type "adaptive" to "enabled" with a calculated budget_tokens
// conversion is needed for Bedrock models that does not support the "adaptive" thinking.type
func (p RequestPayload) convertAdaptiveThinkingForBedrock() (RequestPayload, error) {
thinkingType := gjson.GetBytes(p, messagesReqPathThinkingType)
if thinkingType.String() != constAdaptive {
return p, nil
}
maxTokens := gjson.GetBytes(p, messagesReqPathMaxTokens).Int()
if maxTokens <= 0 {
// max_tokens is required by messages API
return p, xerrors.New("max_tokens: field required")
}
effort := gjson.GetBytes(p, messagesReqPathOutputConfigEffort).String()
// Enabled thinking type requires budget_tokens set.
// Heuristically calculate value based on the effort level.
// Effort-to-ratio mapping adapted from OpenRouter:
// https://openrouter.ai/docs/guides/best-practices/reasoning-tokens#reasoning-effort-level
var ratio float64
switch effort {
case "low":
ratio = 0.2
case "medium":
ratio = 0.5
case "max":
ratio = 0.95
default: // "high" or absent (high is the default effort)
ratio = 0.8
}
// budget_tokens must be ≥ 1024 && < max_tokens. If the calculated budget
// doesn't meet the minimum, disable thinking entirely rather than forcing
// an artificially high budget that would starve the output.
// https://platform.claude.com/docs/en/api/messages/create#create.thinking
// https://platform.claude.com/docs/en/build-with-claude/extended-thinking#how-to-use-extended-thinking
budgetTokens := int64(float64(maxTokens) * ratio)
if budgetTokens < 1024 {
return p.set(messagesReqPathThinking, map[string]string{"type": constDisabled})
}
return p.set(messagesReqPathThinking, map[string]any{
"type": constEnabled,
"budget_tokens": budgetTokens,
})
}
// removeUnsupportedBedrockFields strips top-level fields that Bedrock does not
// support from the payload. Fields that are gated behind a beta flag are only
// removed when the corresponding flag is absent from the Anthropic-Beta header.
// Model-specific beta flags must already be filtered from the header before
// calling this method (see filterBedrockBetaFlags).
func (p RequestPayload) removeUnsupportedBedrockFields(headers http.Header) (RequestPayload, error) {
var payloadMap map[string]any
if err := json.Unmarshal(p, &payloadMap); err != nil {
return p, xerrors.Errorf("failed to unmarshal request payload when removing unsupported Bedrock fields: %w", err)
}
// Always strip unconditionally unsupported fields.
for _, field := range bedrockUnsupportedFields {
delete(payloadMap, field)
}
// Strip beta-gated fields only when their beta flag is missing.
betaValues := headers.Values("Anthropic-Beta")
for field, requiredFlag := range bedrockBetaGatedFields {
if !slices.Contains(betaValues, requiredFlag) {
delete(payloadMap, field)
}
}
result, err := json.Marshal(payloadMap)
if err != nil {
return p, xerrors.Errorf("failed to marshal request payload when removing unsupported Bedrock fields: %w", err)
}
return RequestPayload(result), nil
}
func (p RequestPayload) set(path string, value any) (RequestPayload, error) {
out, err := sjson.SetBytes(p, path, value)
if err != nil {
return p, xerrors.Errorf("set %s: %w", path, err)
}
return RequestPayload(out), nil
}
@@ -0,0 +1,477 @@
package messages //nolint:testpackage // tests unexported internals
import (
"testing"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/shared/constant"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/coder/coder/v2/aibridge/utils"
)
func TestNewRequestPayload(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
requestBody []byte
expectError bool
}{
{
name: "empty body",
requestBody: []byte(" \n\t "),
expectError: true,
},
{
name: "invalid json",
requestBody: []byte(`{"model":`),
expectError: true,
},
{
name: "valid json",
requestBody: []byte(`{"model":"claude-opus-4-5","max_tokens":1024}`),
expectError: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
payload, err := NewRequestPayload(testCase.requestBody)
if testCase.expectError {
require.Error(t, err)
require.Nil(t, payload)
return
}
require.NoError(t, err)
require.Equal(t, RequestPayload(testCase.requestBody), payload)
})
}
}
func TestRequestPayloadStream(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
requestBody string
expectedStream bool
}{
{
name: "stream true",
requestBody: `{"stream":true}`,
expectedStream: true,
},
{
name: "stream false",
requestBody: `{"stream":false}`,
expectedStream: false,
},
{
name: "stream missing",
requestBody: `{}`,
expectedStream: false,
},
{
name: "stream wrong type",
requestBody: `{"stream":"true"}`,
expectedStream: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
payload := mustMessagesPayload(t, testCase.requestBody)
require.Equal(t, testCase.expectedStream, payload.Stream())
})
}
}
func TestRequestPayloadModel(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
requestBody string
expectedModel string
}{
{
name: "model present",
requestBody: `{"model":"claude-opus-4-5"}`,
expectedModel: "claude-opus-4-5",
},
{
name: "model missing",
requestBody: `{}`,
expectedModel: "",
},
{
name: "model wrong type",
requestBody: `{"model":123}`,
expectedModel: "",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
payload := mustMessagesPayload(t, testCase.requestBody)
require.Equal(t, testCase.expectedModel, payload.model())
})
}
}
func TestRequestPayloadLastUserPrompt(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
requestBody string
expectedPrompt string
expectedFound bool
expectError bool
}{
{
name: "last user message string content",
requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"user","content":"hello"}]}`,
expectedPrompt: "hello",
expectedFound: true,
expectError: false,
},
{
name: "last user message typed content returns last text block",
requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"user","content":[{"type":"image","source":{"type":"base64","media_type":"image/png","data":"abc"}},{"type":"text","text":"first"},{"type":"text","text":"last"}]}]}`,
expectedPrompt: "last",
expectedFound: true,
expectError: false,
},
{
name: "last message not from user",
requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"assistant","content":"hello"}]}`,
expectedPrompt: "",
expectedFound: false,
expectError: false,
},
{
name: "no messages key",
requestBody: `{"model":"claude-opus-4-5","max_tokens":1024}`,
expectedPrompt: "",
expectedFound: false,
expectError: false,
},
{
name: "empty messages array",
requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[]}`,
expectedPrompt: "",
expectedFound: false,
expectError: false,
},
{
name: "last user message with empty content array",
requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"user","content":[]}]}`,
expectedPrompt: "",
expectedFound: false,
expectError: false,
},
{
name: "last user message with only non text content",
requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"user","content":[{"type":"image","source":{"type":"base64","media_type":"image/png","data":"abc"}},{"type":"image","source":{"type":"base64","media_type":"image/jpeg","data":"def"}}]}]}`,
expectedPrompt: "",
expectedFound: false,
expectError: false,
},
{
name: "multiple messages with last being user",
requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"user","content":"first"},{"role":"assistant","content":[{"type":"text","text":"response"}]},{"role":"user","content":"second"}]}`,
expectedPrompt: "second",
expectedFound: true,
expectError: false,
},
{
name: "messages wrong type returns error",
requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":{}}`,
expectedPrompt: "",
expectedFound: false,
expectError: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
payload := mustMessagesPayload(t, testCase.requestBody)
prompt, found, err := payload.lastUserPrompt()
if testCase.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, testCase.expectedFound, found)
require.Equal(t, testCase.expectedPrompt, prompt)
})
}
}
func TestRequestPayloadCorrelatingToolCallID(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
requestBody string
expectedToolUseID *string
}{
{
name: "no tool result block",
requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"user","content":"hello"}]}`,
expectedToolUseID: nil,
},
{
name: "returns last tool result from final message",
requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_first","content":"first"},{"type":"tool_result","tool_use_id":"toolu_second","content":"second"}]}]}`,
expectedToolUseID: utils.PtrTo("toolu_second"),
},
{
name: "ignores earlier message tool result",
requestBody: `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_first","content":"first"}]},{"role":"assistant","content":"done"}]}`,
expectedToolUseID: nil,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
payload := mustMessagesPayload(t, testCase.requestBody)
require.Equal(t, testCase.expectedToolUseID, payload.correlatingToolCallID())
})
}
}
func TestRequestPayloadInjectTools(t *testing.T) {
t.Parallel()
payload := mustMessagesPayload(t, `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"user","content":"hello"}],"tools":[{"name":"existing_tool","type":"custom","input_schema":{"type":"object","properties":{}},"cache_control":{"type":"ephemeral"}}]}`)
updatedPayload, err := payload.injectTools([]anthropic.ToolUnionParam{
{
OfTool: &anthropic.ToolParam{
Name: "injected_tool",
Type: anthropic.ToolTypeCustom,
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]interface{}{},
},
},
},
})
require.NoError(t, err)
toolItems := gjson.GetBytes(updatedPayload, "tools").Array()
require.Len(t, toolItems, 2)
require.Equal(t, "injected_tool", toolItems[0].Get("name").String())
require.Equal(t, "existing_tool", toolItems[1].Get("name").String())
require.Equal(t, "ephemeral", toolItems[1].Get("cache_control.type").String())
}
func TestRequestPayloadConvertAdaptiveThinkingForBedrock(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
requestBody string
expectedThinkingType string
expectedBudgetTokens int64
expectError bool
}{
{
name: "no_thinking_field_is_no_op",
requestBody: `{"model":"claude-sonnet-4-5","max_tokens":10000,"messages":[]}`,
expectedThinkingType: "",
},
{
name: "non_adaptive_thinking_type_is_no_op",
requestBody: `{"model":"claude-sonnet-4-5","max_tokens":10000,"thinking":{"type":"enabled","budget_tokens":5000},"messages":[]}`,
expectedThinkingType: "enabled",
expectedBudgetTokens: 5000,
},
{
name: "adaptive_with_no_effort_defaults_to_80%",
requestBody: `{"model":"claude-sonnet-4-5","max_tokens":10000,"thinking":{"type":"adaptive"},"messages":[]}`,
expectedThinkingType: "enabled",
expectedBudgetTokens: 8000, // 10000 * 0.8 (default/high effort)
},
{
name: "adaptive_with_explicit_effort_uses_correct_percentage",
requestBody: `{"model":"claude-sonnet-4-5","max_tokens":10000,"thinking":{"type":"adaptive"},"output_config":{"effort":"low"},"messages":[]}`,
expectedThinkingType: "enabled",
expectedBudgetTokens: 2000, // 10000 * 0.2
},
{
name: "adaptive_disables_thinking_when_budget_below_minimum",
requestBody: `{"model":"claude-sonnet-4-5","max_tokens":512,"thinking":{"type":"adaptive"},"messages":[]}`,
expectedThinkingType: "disabled", // 512 * 0.8 = 409, below 1024 minimum
},
{
name: "adaptive_without_max_tokens_returns_error",
requestBody: `{"model":"claude-sonnet-4-5","thinking":{"type":"adaptive"},"messages":[]}`,
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
payload := mustMessagesPayload(t, tc.requestBody)
updatedPayload, err := payload.convertAdaptiveThinkingForBedrock()
if tc.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
thinking := gjson.GetBytes(updatedPayload, messagesReqPathThinking)
require.NotEqual(t, tc.expectedThinkingType == "", thinking.Exists(), "thinking should not be set")
require.Equal(t, tc.expectedThinkingType, gjson.GetBytes(updatedPayload, messagesReqPathThinkingType).String()) // non existing field returns zero value
budgetTokens := gjson.GetBytes(updatedPayload, messagesReqPathThinkingBudgetTokens)
require.NotEqual(t, tc.expectedBudgetTokens == 0, budgetTokens.Exists(), "budget_tokens should not be set")
require.Equal(t, tc.expectedBudgetTokens, budgetTokens.Int()) // non existing field returns zero value
})
}
}
func TestRequestPayloadDisableParallelToolCalls(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
requestBody string
expectError string
expectedType string
expectedDisableParallel *bool
}{
{
name: "defaults to auto when missing",
requestBody: `{"model":"claude-opus-4-5","max_tokens":1024}`,
expectedType: string(constant.ValueOf[constant.Auto]()),
expectedDisableParallel: utils.PtrTo(true),
},
{
name: "auto gets disabled",
requestBody: `{"tool_choice":{"type":"auto"}}`,
expectedType: string(constant.ValueOf[constant.Auto]()),
expectedDisableParallel: utils.PtrTo(true),
},
{
name: "any gets disabled",
requestBody: `{"tool_choice":{"type":"any"}}`,
expectedType: string(constant.ValueOf[constant.Any]()),
expectedDisableParallel: utils.PtrTo(true),
},
{
name: "tool gets disabled",
requestBody: `{"tool_choice":{"type":"tool","name":"abc"}}`,
expectedType: string(constant.ValueOf[constant.Tool]()),
expectedDisableParallel: utils.PtrTo(true),
},
{
name: "none remains unchanged",
requestBody: `{"tool_choice":{"type":"none"}}`,
expectedType: string(constant.ValueOf[constant.None]()),
expectedDisableParallel: nil,
},
{
name: "empty type defaults to auto",
requestBody: `{"tool_choice":{}}`,
expectedType: string(constant.ValueOf[constant.Auto]()),
expectedDisableParallel: utils.PtrTo(true),
},
{
name: "non-object tool_choice returns error",
requestBody: `{"tool_choice":"auto"}`,
expectError: "unsupported tool_choice type",
},
{
name: "non-string tool_choice type returns error",
requestBody: `{"tool_choice":{"type":123}}`,
expectError: "unsupported tool_choice.type type",
},
{
name: "unsupported tool_choice type returns error",
requestBody: `{"tool_choice":{"type":"unknown"}}`,
expectError: "unsupported tool_choice.type value",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
payload := mustMessagesPayload(t, testCase.requestBody)
updatedPayload, err := payload.disableParallelToolCalls()
if testCase.expectError != "" {
require.ErrorContains(t, err, testCase.expectError)
return
}
require.NoError(t, err)
toolChoice := gjson.GetBytes(updatedPayload, "tool_choice")
require.Equal(t, testCase.expectedType, toolChoice.Get("type").String())
disableParallelResult := toolChoice.Get("disable_parallel_tool_use")
if testCase.expectedDisableParallel == nil {
require.False(t, disableParallelResult.Exists())
return
}
require.True(t, disableParallelResult.Exists())
require.Equal(t, *testCase.expectedDisableParallel, disableParallelResult.Bool())
})
}
}
func TestRequestPayloadAppendedMessages(t *testing.T) {
t.Parallel()
payload := mustMessagesPayload(t, `{"model":"claude-opus-4-5","max_tokens":1024,"messages":[{"role":"user","content":"hello"}]}`)
updatedPayload, err := payload.appendedMessages([]anthropic.MessageParam{
{
Role: anthropic.MessageParamRoleAssistant,
Content: []anthropic.ContentBlockParamUnion{
anthropic.NewTextBlock("assistant response"),
},
},
anthropic.NewUserMessage(anthropic.NewToolResultBlock("toolu_123", "tool output", false)),
})
require.NoError(t, err)
messageItems := gjson.GetBytes(updatedPayload, "messages").Array()
require.Len(t, messageItems, 3)
require.Equal(t, "hello", messageItems[0].Get("content").String())
require.Equal(t, "assistant", messageItems[1].Get("role").String())
require.Equal(t, "assistant response", messageItems[1].Get("content.0.text").String())
require.Equal(t, "tool_result", messageItems[2].Get("content.0.type").String())
require.Equal(t, "toolu_123", messageItems[2].Get("content.0.tool_use_id").String())
}
+593
View File
@@ -0,0 +1,593 @@
package messages
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
"github.com/anthropics/anthropic-sdk-go/packages/ssestream"
"github.com/anthropics/anthropic-sdk-go/shared/constant"
"github.com/google/uuid"
mcplib "github.com/mark3labs/mcp-go/mcp"
"github.com/tidwall/sjson"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/config"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/intercept"
"github.com/coder/coder/v2/aibridge/intercept/eventstream"
"github.com/coder/coder/v2/aibridge/mcp"
"github.com/coder/coder/v2/aibridge/recorder"
"github.com/coder/coder/v2/aibridge/tracing"
"github.com/coder/quartz"
)
type StreamingInterception struct {
interceptionBase
}
func NewStreamingInterceptor(
id uuid.UUID,
reqPayload RequestPayload,
providerName string,
cfg config.Anthropic,
bedrockCfg *config.AWSBedrock,
clientHeaders http.Header,
authHeaderName string,
tracer trace.Tracer,
cred intercept.CredentialInfo,
) *StreamingInterception {
return &StreamingInterception{interceptionBase: interceptionBase{
id: id,
providerName: providerName,
reqPayload: reqPayload,
cfg: cfg,
bedrockCfg: bedrockCfg,
clientHeaders: clientHeaders,
authHeaderName: authHeaderName,
tracer: tracer,
credential: cred,
}}
}
func (i *StreamingInterception) Setup(logger slog.Logger, rec recorder.Recorder, mcpProxy mcp.ServerProxier) {
i.interceptionBase.Setup(logger.Named("streaming"), rec, mcpProxy)
}
func (*StreamingInterception) Streaming() bool {
return true
}
func (i *StreamingInterception) TraceAttributes(r *http.Request) []attribute.KeyValue {
return i.interceptionBase.baseTraceAttributes(r, true)
}
// ProcessRequest handles a request to /v1/messages.
// This API has a state-machine behind it, which is described in https://docs.claude.com/en/docs/build-with-claude/streaming#event-types.
//
// Each stream uses the following event flow:
// - `message_start`: contains a Message object with empty content.
// - A series of content blocks, each of which have a `content_block_start`, one or more `content_block_delta` events, and a `content_block_stop` event.
// - Each content block will have an index that corresponds to its index in the final Message content array.
// - One or more `message_delta` events, indicating top-level changes to the final Message object.
// - A final `message_stop` event.
//
// It will inject any tools which have been provided by the [mcp.ServerProxier].
//
// When a response from the server includes an event indicating that a tool must be invoked, a conditional
// flow takes place:
//
// a) if the tool is not injected (i.e. defined by the client), relay the event unmodified
// b) if the tool is injected, it will be invoked by the [mcp.ServerProxier] in the remote MCP server, and its
// results relayed to the SERVER. The response from the server will be handled synchronously, and this loop
// can continue until all injected tool invocations are completed and the response is relayed to the client.
func (i *StreamingInterception) ProcessRequest(w http.ResponseWriter, r *http.Request) (outErr error) {
if len(i.reqPayload) == 0 {
return xerrors.New("developer error: request payload is empty")
}
ctx, span := i.tracer.Start(r.Context(), "Intercept.ProcessRequest", trace.WithAttributes(tracing.InterceptionAttributesFromContext(r.Context())...))
defer tracing.EndSpanErr(span, &outErr)
// Allow us to interrupt watch via cancel.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
r = r.WithContext(ctx) // Rewire context for SSE cancellation.
logger := i.logger.With(slog.F("model", i.Model()))
var (
prompt string
promptFound bool
err error
)
prompt, promptFound, err = i.reqPayload.lastUserPrompt()
if err != nil {
logger.Warn(ctx, "failed to determine last user prompt", slog.Error(err))
}
// Claude Code uses a "small/fast model" for certain tasks.
if !i.isSmallFastModel() {
// Only inject tools into "actual" request.
i.injectTools()
}
streamCtx, streamCancel := context.WithCancelCause(ctx)
defer streamCancel(xerrors.New("deferred"))
// TODO(ssncferreira): inject actor headers directly in the client-header
// middleware instead of using SDK options.
var opts []option.RequestOption
if actor := aibcontext.ActorFromContext(ctx); actor != nil && i.cfg.SendActorHeaders {
opts = append(opts, intercept.ActorHeadersAsAnthropicOpts(actor)...)
}
svc, err := i.newMessagesService(streamCtx, opts...)
if err != nil {
err = xerrors.Errorf("create anthropic client: %w", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return err
}
// events will either terminate when shutdown after interaction with upstream completes, or when streamCtx is done.
events := eventstream.NewEventStream(streamCtx, logger.Named("sse-sender"), i.pingPayload(), quartz.NewReal())
go events.Start(w, r)
defer func() {
_ = events.Shutdown(streamCtx) // Catch-all in case it doesn't get shutdown after stream completes.
}()
// Accumulate usage across the entire streaming interaction (including tool reinvocations).
var cumulativeUsage anthropic.Usage
var lastErr error
var interceptionErr error
isFirst := true
newStream:
for {
// TODO add outer loop span (https://github.com/coder/aibridge/issues/67)
if err := streamCtx.Err(); err != nil {
interceptionErr = xerrors.Errorf("stream exit: %w", err)
break
}
stream := i.newStream(streamCtx, svc)
var message anthropic.Message
var lastToolName string
pendingToolCalls := make(map[string]string)
for stream.Next() {
event := stream.Current()
if err := message.Accumulate(event); err != nil {
logger.Warn(ctx, "failed to accumulate streaming events", slog.Error(err), slog.F("event", event), slog.F("msg", message.RawJSON()))
lastErr = xerrors.Errorf("accumulate event: %w", err)
break
}
// Tool-related handling.
switch event.Type {
case string(constant.ValueOf[constant.ContentBlockStart]()):
if block, ok := event.AsContentBlockStart().ContentBlock.AsAny().(anthropic.ToolUseBlock); ok {
lastToolName = block.Name
if i.mcpProxy != nil && i.mcpProxy.GetTool(block.Name) != nil {
pendingToolCalls[block.Name] = block.ID
// Don't relay this event back, otherwise the client will try invoke the tool as well.
continue
}
}
case string(constant.ValueOf[constant.ContentBlockDelta]()):
if len(pendingToolCalls) > 0 && i.mcpProxy != nil && i.mcpProxy.GetTool(lastToolName) != nil {
// We're busy with a tool call, don't relay this event back.
continue
}
case string(constant.ValueOf[constant.ContentBlockStop]()):
// Reset the tool name
isInjected := i.mcpProxy != nil && i.mcpProxy.GetTool(lastToolName) != nil
lastToolName = ""
if len(pendingToolCalls) > 0 && isInjected {
// We're busy with a tool call, don't relay this event back.
continue
}
case string(constant.ValueOf[constant.MessageStart]()):
start := event.AsMessageStart()
accumulateUsage(&cumulativeUsage, start.Message.Usage)
_ = i.recorder.RecordTokenUsage(streamCtx, &recorder.TokenUsageRecord{
InterceptionID: i.ID().String(),
MsgID: message.ID,
Input: start.Message.Usage.InputTokens,
Output: start.Message.Usage.OutputTokens,
CacheReadInputTokens: start.Message.Usage.CacheReadInputTokens,
CacheWriteInputTokens: start.Message.Usage.CacheCreationInputTokens,
ExtraTokenTypes: map[string]int64{
"web_search_requests": start.Message.Usage.ServerToolUse.WebSearchRequests,
"cache_creation_input": start.Message.Usage.CacheCreationInputTokens, // TODO: remove from ExtraTokenTypes (https://github.com/coder/aibridge/issues/243)
"cache_read_input": start.Message.Usage.CacheReadInputTokens, // TODO: remove from ExtraTokenTypes (https://github.com/coder/aibridge/issues/243)
"cache_ephemeral_1h_input": start.Message.Usage.CacheCreation.Ephemeral1hInputTokens,
"cache_ephemeral_5m_input": start.Message.Usage.CacheCreation.Ephemeral5mInputTokens,
},
})
if !isFirst {
// Don't send message_start unless first message!
// We're sending multiple messages back and forth with the API, but from the client's perspective
// they're just expecting a single message.
continue
}
case string(constant.ValueOf[constant.MessageDelta]()):
delta := event.AsMessageDelta()
accumulateUsage(&cumulativeUsage, delta.Usage)
// Only output tokens should change in message_delta.
_ = i.recorder.RecordTokenUsage(streamCtx, &recorder.TokenUsageRecord{
InterceptionID: i.ID().String(),
MsgID: message.ID,
Output: delta.Usage.OutputTokens,
})
// Don't relay message_delta events which indicate injected tool use.
if len(pendingToolCalls) > 0 && i.mcpProxy != nil && i.mcpProxy.GetTool(lastToolName) != nil {
continue
}
// If currently calling a tool.
if len(message.Content) > 0 && message.Content[len(message.Content)-1].Type == string(constant.ValueOf[constant.ToolUse]()) {
toolName := message.Content[len(message.Content)-1].AsToolUse().Name
if len(pendingToolCalls) > 0 && i.mcpProxy != nil && i.mcpProxy.GetTool(toolName) != nil {
continue
}
}
// We should be updating the event's usage to the calculated cumulative usage. However...
// the SDK only accumulates output tokens on message_delta, since that's all that *should* change.
//
// Backstory: the API reports tokens during message_start AND message_delta. message_start reports the input
// tokens and others, while the delta should only report changes to output tokens.
// HOWEVER, when we invoke injected tools we're starting a whole new message (and subsequently receive
// message_start and message_delta events), and the previous message_start has already been relayed, so in effect
// we can't really modify anything other than output tokens here according to the SDK.
// This will affect how the client reports token usage for input tokens, for example.
// For our purposes, the server (aibridge) is authoritative anyway so it's not a big deal, but this is something to note.
//
// See https://github.com/anthropics/anthropic-sdk-go/blob/v1.12.0/message.go#L2619-L2622
event.Usage.OutputTokens = cumulativeUsage.OutputTokens
// Don't send message_stop until all tools have been called.
case string(constant.ValueOf[constant.MessageStop]()):
// Capture any thinking blocks that were returned.
for _, t := range i.extractModelThoughts(&message) {
_ = i.recorder.RecordModelThought(ctx, &recorder.ModelThoughtRecord{
InterceptionID: i.ID().String(),
Content: t.Content,
Metadata: t.Metadata,
})
}
// Process injected tools.
if len(pendingToolCalls) > 0 {
// Append the whole message from this stream as context since we'll be sending a new request with the tool results.
var loopMessages []anthropic.MessageParam
loopMessages = append(loopMessages, message.ToParam())
for name, id := range pendingToolCalls {
if i.mcpProxy == nil {
continue
}
if i.mcpProxy.GetTool(name) == nil {
// Not an MCP proxy call, don't do anything.
continue
}
tool := i.mcpProxy.GetTool(name)
if tool == nil {
logger.Warn(ctx, "tool not found in manager", slog.F("tool_name", name))
continue
}
var (
input json.RawMessage
foundTool bool
foundTools int
)
for _, block := range message.Content {
if variant, ok := block.AsAny().(anthropic.ToolUseBlock); ok {
foundTools++
if variant.Name == name {
input = variant.Input
foundTool = true
}
}
}
if !foundTool {
logger.Warn(ctx, "failed to find tool input", slog.F("tool_name", name), slog.F("found_tools", foundTools))
continue
}
res, err := tool.Call(streamCtx, input, i.tracer)
_ = i.recorder.RecordToolUsage(streamCtx, &recorder.ToolUsageRecord{
InterceptionID: i.ID().String(),
MsgID: message.ID,
ToolCallID: id,
ServerURL: &tool.ServerURL,
Tool: tool.Name,
Args: input,
Injected: true,
InvocationError: err,
})
if err != nil {
// Always provide a tool_result even if the tool call failed
loopMessages = append(loopMessages,
anthropic.NewUserMessage(anthropic.NewToolResultBlock(id, fmt.Sprintf("Error calling tool: %v", err), true)),
)
continue
}
// Process tool result
toolResult := anthropic.ContentBlockParamUnion{
OfToolResult: &anthropic.ToolResultBlockParam{
ToolUseID: id,
IsError: anthropic.Bool(false),
},
}
var hasValidResult bool
for _, content := range res.Content {
switch cb := content.(type) {
case mcplib.TextContent:
toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.ToolResultBlockParamContentUnion{
OfText: &anthropic.TextBlockParam{
Text: cb.Text,
},
})
hasValidResult = true
case mcplib.EmbeddedResource:
switch resource := cb.Resource.(type) {
case mcplib.TextResourceContents:
val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s",
resource.MIMEType, resource.URI, resource.Text)
toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.ToolResultBlockParamContentUnion{
OfText: &anthropic.TextBlockParam{
Text: val,
},
})
hasValidResult = true
case mcplib.BlobResourceContents:
val := fmt.Sprintf("Binary resource (MIME: %s, URI: %s): %s",
resource.MIMEType, resource.URI, resource.Blob)
toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.ToolResultBlockParamContentUnion{
OfText: &anthropic.TextBlockParam{
Text: val,
},
})
hasValidResult = true
default:
logger.Warn(ctx, "unknown embedded resource type", slog.F("type", fmt.Sprintf("%T", resource)))
toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.ToolResultBlockParamContentUnion{
OfText: &anthropic.TextBlockParam{
Text: "Error: unknown embedded resource type",
},
})
toolResult.OfToolResult.IsError = anthropic.Bool(true)
hasValidResult = true
}
default:
logger.Warn(ctx, "not handling non-text tool result", slog.F("type", fmt.Sprintf("%T", cb)))
toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.ToolResultBlockParamContentUnion{
OfText: &anthropic.TextBlockParam{
Text: "Error: unsupported tool result type",
},
})
toolResult.OfToolResult.IsError = anthropic.Bool(true)
hasValidResult = true
}
}
// If no content was processed, still add a tool_result
if !hasValidResult {
logger.Warn(ctx, "no tool result added", slog.F("content_len", len(res.Content)), slog.F("is_error", res.IsError))
toolResult.OfToolResult.Content = append(toolResult.OfToolResult.Content, anthropic.ToolResultBlockParamContentUnion{
OfText: &anthropic.TextBlockParam{
Text: "Error: no valid tool result content",
},
})
toolResult.OfToolResult.IsError = anthropic.Bool(true)
}
if len(toolResult.OfToolResult.Content) > 0 {
loopMessages = append(loopMessages, anthropic.NewUserMessage(toolResult))
}
}
// Sync the raw payload with updated messages so that withBody()
// sends the updated payload on the next iteration.
updatedPayload, syncErr := i.reqPayload.appendedMessages(loopMessages)
if syncErr != nil {
lastErr = xerrors.Errorf("sync payload for agentic loop: %w", syncErr)
break
}
i.reqPayload = updatedPayload
// Causes a new stream to be run with updated messages.
isFirst = false
continue newStream
}
// Find all the non-injected tools and track their uses.
for _, block := range message.Content {
if variant, ok := block.AsAny().(anthropic.ToolUseBlock); ok {
if i.mcpProxy != nil && i.mcpProxy.GetTool(variant.Name) != nil {
continue
}
_ = i.recorder.RecordToolUsage(streamCtx, &recorder.ToolUsageRecord{
InterceptionID: i.ID().String(),
MsgID: message.ID,
ToolCallID: variant.ID,
Tool: variant.Name,
Args: variant.Input,
Injected: false,
})
}
}
}
// Overwrite response identifier since proxy obscures injected tool call invocations.
payload, err := i.marshalEvent(event)
if err != nil {
logger.Warn(ctx, "failed to marshal event", slog.Error(err), slog.F("event", event.RawJSON()))
lastErr = xerrors.Errorf("marshal event: %w", err)
break
}
if err := events.Send(streamCtx, payload); err != nil {
if eventstream.IsUnrecoverableError(err) {
logger.Debug(ctx, "processing terminated", slog.Error(err))
break // Stop processing if client disconnected or context canceled.
}
logger.Warn(ctx, "failed to relay event", slog.Error(err))
lastErr = xerrors.Errorf("relay event: %w", err)
break
}
}
if promptFound {
_ = i.recorder.RecordPromptUsage(ctx, &recorder.PromptUsageRecord{
InterceptionID: i.ID().String(),
MsgID: message.ID,
Prompt: prompt,
})
prompt = "" //nolint:ineffassign // reset to prevent double-recording across newStream iterations
promptFound = false //nolint:ineffassign // reset to prevent double-recording across newStream iterations
}
if events.IsStreaming() {
// Check if the stream encountered any errors.
if streamErr := stream.Err(); streamErr != nil {
if eventstream.IsUnrecoverableError(streamErr) {
logger.Debug(ctx, "stream terminated", slog.Error(streamErr))
// We can't reflect an error back if there's a connection error or the request context was canceled.
} else if antErr := getErrorResponse(streamErr); antErr != nil {
logger.Warn(ctx, "anthropic stream error", slog.Error(streamErr))
interceptionErr = antErr
} else {
logger.Warn(ctx, "unknown stream error", slog.Error(streamErr))
// Unfortunately, the Anthropic SDK does not support parsing errors received in the stream
// into known types (i.e. [shared.OverloadedError]).
// See https://github.com/anthropics/anthropic-sdk-go/blob/v1.12.0/packages/ssestream/ssestream.go#L172-L174
// All it does is wrap the payload in an error - which is all we can return, currently.
interceptionErr = newErrorResponse(xerrors.Errorf("unknown stream error: %w", streamErr))
}
} else if lastErr != nil {
// Otherwise check if any logical errors occurred during processing.
logger.Warn(ctx, "stream processing failed", slog.Error(lastErr))
interceptionErr = newErrorResponse(xerrors.Errorf("processing error: %w", lastErr))
}
if interceptionErr != nil {
payload, err := i.marshal(interceptionErr)
if err != nil {
logger.Warn(ctx, "failed to marshal error", slog.Error(err), slog.F("error_payload", fmt.Sprintf("%+v", interceptionErr)))
} else if err := events.Send(streamCtx, payload); err != nil {
logger.Warn(ctx, "failed to relay error", slog.Error(err), slog.F("payload", payload))
}
}
} else {
// Stream has not started yet; write to response if present.
i.writeUpstreamError(w, getErrorResponse(stream.Err()))
}
shutdownCtx, shutdownCancel := context.WithTimeout(ctx, time.Second*30)
// Give the events stream 30 seconds (TODO: configurable) to gracefully shutdown.
if err := events.Shutdown(shutdownCtx); err != nil {
logger.Warn(ctx, "event stream shutdown", slog.Error(err))
}
shutdownCancel()
// Cancel the stream context, we're now done.
if interceptionErr != nil {
streamCancel(interceptionErr)
} else {
streamCancel(xerrors.New("gracefully done"))
}
break
}
return interceptionErr
}
func (i *StreamingInterception) marshalEvent(event anthropic.MessageStreamEventUnion) ([]byte, error) {
sj, err := sjson.Set(event.RawJSON(), "message.id", i.ID().String())
if err != nil {
return nil, xerrors.Errorf("marshal event id failed: %w", err)
}
sj, err = sjson.Set(sj, "usage.output_tokens", event.Usage.OutputTokens)
if err != nil {
return nil, xerrors.Errorf("marshal event usage failed: %w", err)
}
return i.encodeForStream([]byte(sj), event.Type), nil
}
func (i *StreamingInterception) marshal(payload any) ([]byte, error) {
data, err := json.Marshal(payload)
if err != nil {
return nil, xerrors.Errorf("marshal payload: %w", err)
}
var parsed map[string]any
if err := json.Unmarshal(data, &parsed); err != nil {
return nil, xerrors.Errorf("unmarshal payload: %w", err)
}
eventType, ok := parsed["type"].(string)
if !ok || strings.TrimSpace(eventType) == "" {
return nil, xerrors.Errorf("could not determine type from payload %q", data)
}
return i.encodeForStream(data, eventType), nil
}
// https://docs.anthropic.com/en/docs/build-with-claude/streaming#basic-streaming-request
func (i *StreamingInterception) pingPayload() []byte {
return i.encodeForStream([]byte(`{"type": "ping"}`), "ping")
}
func (*StreamingInterception) encodeForStream(payload []byte, typ string) []byte {
// bytes.Buffer writes to in-memory storage and never return errors.
var buf bytes.Buffer
_, _ = buf.WriteString("event: ")
_, _ = buf.WriteString(typ)
_, _ = buf.WriteString("\n")
_, _ = buf.WriteString("data: ")
_, _ = buf.Write(payload)
_, _ = buf.WriteString("\n\n")
return buf.Bytes()
}
// newStream traces svc.NewStreaming() call.
func (i *StreamingInterception) newStream(ctx context.Context, svc anthropic.MessageService) *ssestream.Stream[anthropic.MessageStreamEventUnion] {
_, span := i.tracer.Start(ctx, "Intercept.ProcessRequest.Upstream", trace.WithAttributes(tracing.InterceptionAttributesFromContext(ctx)...))
defer span.End()
return svc.NewStreaming(ctx, anthropic.MessageNewParams{}, i.withBody())
}
+416
View File
@@ -0,0 +1,416 @@
package responses
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/google/uuid"
"github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/responses"
"github.com/openai/openai-go/v3/shared/constant"
"github.com/tidwall/gjson"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/config"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/intercept"
"github.com/coder/coder/v2/aibridge/intercept/apidump"
"github.com/coder/coder/v2/aibridge/mcp"
"github.com/coder/coder/v2/aibridge/recorder"
"github.com/coder/coder/v2/aibridge/tracing"
"github.com/coder/quartz"
)
const (
requestTimeout = time.Second * 600
)
type responsesInterceptionBase struct {
id uuid.UUID
providerName string
// clientHeaders are the original HTTP headers from the client request.
clientHeaders http.Header
authHeaderName string
reqPayload RequestPayload
cfg config.OpenAI
recorder recorder.Recorder
mcpProxy mcp.ServerProxier
logger slog.Logger
tracer trace.Tracer
credential intercept.CredentialInfo
}
func (i *responsesInterceptionBase) newResponsesService() responses.ResponseService {
opts := []option.RequestOption{option.WithBaseURL(i.cfg.BaseURL), option.WithAPIKey(i.cfg.Key)}
if i.cfg.MaxRetries != nil {
opts = append(opts, option.WithMaxRetries(*i.cfg.MaxRetries))
}
// Add extra headers if configured.
// Some providers require additional headers that are not added by the SDK.
// TODO(ssncferreira): remove as part of https://github.com/coder/aibridge/issues/192
for key, value := range i.cfg.ExtraHeaders {
opts = append(opts, option.WithHeader(key, value))
}
// Forward client headers to upstream. This middleware runs after the SDK
// has built the request, and replaces the outgoing headers with the sanitized
// client headers plus provider auth.
if i.clientHeaders != nil {
opts = append(opts, option.WithMiddleware(func(req *http.Request, next option.MiddlewareNext) (*http.Response, error) {
req.Header = intercept.BuildUpstreamHeaders(req.Header, i.clientHeaders, i.authHeaderName)
return next(req)
}))
}
// Add API dump middleware if configured
if mw := apidump.NewBridgeMiddleware(i.cfg.APIDumpDir, i.providerName, i.Model(), i.id, i.logger, quartz.NewReal()); mw != nil {
opts = append(opts, option.WithMiddleware(mw))
}
return responses.NewResponseService(opts...)
}
func (i *responsesInterceptionBase) ID() uuid.UUID {
return i.id
}
func (i *responsesInterceptionBase) Credential() intercept.CredentialInfo {
return i.credential
}
func (i *responsesInterceptionBase) Setup(logger slog.Logger, rec recorder.Recorder, mcpProxy mcp.ServerProxier) {
i.logger = logger.With(slog.F("model", i.Model()))
i.recorder = rec
i.mcpProxy = mcpProxy
}
func (i *responsesInterceptionBase) Model() string {
return i.reqPayload.model()
}
func (i *responsesInterceptionBase) CorrelatingToolCallID() *string {
return i.reqPayload.correlatingToolCallID()
}
func (i *responsesInterceptionBase) baseTraceAttributes(r *http.Request, streaming bool) []attribute.KeyValue {
return []attribute.KeyValue{
attribute.String(tracing.RequestPath, r.URL.Path),
attribute.String(tracing.InterceptionID, i.id.String()),
attribute.String(tracing.InitiatorID, aibcontext.ActorIDFromContext(r.Context())),
attribute.String(tracing.Provider, i.providerName),
attribute.String(tracing.Model, i.Model()),
attribute.Bool(tracing.Streaming, streaming),
}
}
func (i *responsesInterceptionBase) validateRequest(ctx context.Context, w http.ResponseWriter) error {
if i.reqPayload.background() {
err := xerrors.New("background requests are currently not supported by AI Bridge")
i.sendCustomErr(ctx, w, http.StatusNotImplemented, err)
return err
}
return nil
}
// sendCustomErr sends custom responses.Error error to the client
// it should only be called before any data is sent back to the client
func (i *responsesInterceptionBase) sendCustomErr(ctx context.Context, w http.ResponseWriter, code int, err error) {
// Same JSON shape as responses.Error but using a plain struct because
// responses.Error embeds *http.Request whose GetBody func field
// is not JSON-marshalable (SA1026).
respErr := struct {
Code string `json:"code"`
Message string `json:"message"`
}{
Code: strconv.Itoa(code),
Message: err.Error(),
}
if b, err := json.Marshal(respErr); err != nil {
i.logger.Warn(ctx, "failed to marshal custom error: ", slog.Error(err))
} else {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
if _, err := w.Write(b); err != nil {
i.logger.Warn(ctx, "failed to send custom error: ", slog.Error(err))
}
}
}
func (i *responsesInterceptionBase) requestOptions(respCopy *responseCopier) []option.RequestOption {
opts := []option.RequestOption{
// Sends original payload to solve json re-encoding issues
// eg. Codex CLI produces requests without ID set in reasoning items: https://platform.openai.com/docs/api-reference/responses/create#responses_create-input-input_item_list-item-reasoning-id
// when re-encoded, ID field is set to empty string which results
// in bad request while not sending ID field at all somehow works.
option.WithRequestBody("application/json", []byte(i.reqPayload)),
// copyMiddleware copies body of original response body to the buffer in responseCopier,
// also reference to headers and status code is kept responseCopier.
// responseCopier is used by interceptors to forward response as it was received,
// eliminating any possibility of JSON re-encoding issues.
option.WithMiddleware(respCopy.copyMiddleware),
}
if !i.reqPayload.Stream() {
opts = append(opts, option.WithRequestTimeout(requestTimeout))
}
return opts
}
func (i *responsesInterceptionBase) recordUserPrompt(ctx context.Context, responseID string, prompt string) {
if responseID == "" {
i.logger.Warn(ctx, "got empty response ID, skipping prompt recording")
return
}
promptUsage := &recorder.PromptUsageRecord{
InterceptionID: i.ID().String(),
MsgID: responseID,
Prompt: prompt,
}
if err := i.recorder.RecordPromptUsage(ctx, promptUsage); err != nil {
i.logger.Warn(ctx, "failed to record prompt usage", slog.Error(err))
}
}
func (i *responsesInterceptionBase) recordModelThoughts(ctx context.Context, response *responses.Response) {
for _, t := range i.extractModelThoughts(response) {
_ = i.recorder.RecordModelThought(ctx, &recorder.ModelThoughtRecord{
InterceptionID: i.ID().String(),
Content: t.Content,
Metadata: t.Metadata,
})
}
}
func (i *responsesInterceptionBase) recordNonInjectedToolUsage(ctx context.Context, response *responses.Response) {
if response == nil {
i.logger.Warn(ctx, "got empty response, skipping tool usage recording")
return
}
for _, item := range response.Output {
var args recorder.ToolArgs
// recording other function types to be considered: https://github.com/coder/aibridge/issues/121
switch item.Type {
case string(constant.ValueOf[constant.FunctionCall]()):
args = i.parseFunctionCallJSONArgs(ctx, item.Arguments)
case string(constant.ValueOf[constant.CustomToolCall]()):
args = item.Input
default:
continue
}
if err := i.recorder.RecordToolUsage(ctx, &recorder.ToolUsageRecord{
InterceptionID: i.ID().String(),
MsgID: response.ID,
ToolCallID: item.CallID,
Tool: item.Name,
Args: args,
Injected: false,
}); err != nil {
i.logger.Warn(ctx, "failed to record tool usage", slog.Error(err), slog.F("tool", item.Name))
}
}
}
func (i *responsesInterceptionBase) parseFunctionCallJSONArgs(ctx context.Context, raw string) recorder.ToolArgs {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return trimmed
}
var args recorder.ToolArgs
if err := json.Unmarshal([]byte(trimmed), &args); err != nil {
i.logger.Warn(ctx, "failed to unmarshal tool args", slog.Error(err))
return trimmed
}
return args
}
func (i *responsesInterceptionBase) recordTokenUsage(ctx context.Context, response *responses.Response) {
if response == nil {
i.logger.Warn(ctx, "got empty response, skipping token usage recording")
return
}
usage := response.Usage
// Keeping logic consistent with chat completions
// Input *includes* the cached tokens, so we subtract them here to reflect actual input token usage.
inputNonCacheTokens := usage.InputTokens - usage.InputTokensDetails.CachedTokens
if err := i.recorder.RecordTokenUsage(ctx, &recorder.TokenUsageRecord{
InterceptionID: i.ID().String(),
MsgID: response.ID,
Input: inputNonCacheTokens,
Output: usage.OutputTokens,
CacheReadInputTokens: usage.InputTokensDetails.CachedTokens,
ExtraTokenTypes: map[string]int64{
"input_cached": usage.InputTokensDetails.CachedTokens, // TODO: remove from ExtraTokenTypes (https://github.com/coder/aibridge/issues/243)
"output_reasoning": usage.OutputTokensDetails.ReasoningTokens,
"total_tokens": usage.TotalTokens,
},
}); err != nil {
i.logger.Warn(ctx, "failed to record token usage", slog.Error(err))
}
}
// extractModelThoughts extracts model thoughts from response output items.
// It captures both reasoning summary items and commentary messages (message
// output items with "phase": "commentary") as model thoughts.
func (*responsesInterceptionBase) extractModelThoughts(response *responses.Response) []*recorder.ModelThoughtRecord {
if response == nil {
return nil
}
var thoughts []*recorder.ModelThoughtRecord
for _, item := range response.Output {
switch item.Type {
case string(constant.ValueOf[constant.Reasoning]()):
reasoning := item.AsReasoning()
for _, summary := range reasoning.Summary {
if summary.Text == "" {
continue
}
thoughts = append(thoughts, &recorder.ModelThoughtRecord{
Content: summary.Text,
Metadata: recorder.Metadata{"source": recorder.ThoughtSourceReasoningSummary},
})
}
case string(constant.ValueOf[constant.Message]()):
// The API sometimes returns commentary messages instead of reasoning
// summaries. These are assistant message output items with "phase": "commentary".
// The SDK doesn't expose a Phase field, so we extract it from raw JSON.
// TODO: revisit when the OpenAI SDK adds a proper Phase field.
raw := item.RawJSON()
if gjson.Get(raw, "role").String() != string(constant.ValueOf[constant.Assistant]()) ||
gjson.Get(raw, "phase").String() != "commentary" {
continue
}
msg := item.AsMessage()
for _, part := range msg.Content {
if part.Type != string(constant.ValueOf[constant.OutputText]()) {
continue
}
if part.Text == "" {
continue
}
thoughts = append(thoughts, &recorder.ModelThoughtRecord{
Content: part.Text,
Metadata: recorder.Metadata{"source": recorder.ThoughtSourceCommentary},
})
}
}
}
return thoughts
}
func (i *responsesInterceptionBase) hasInjectableTools() bool {
return i.mcpProxy != nil && len(i.mcpProxy.ListTools()) > 0
}
// responseCopier helper struct to send original response to the client
type responseCopier struct {
buff deltaBuffer
responseStatus int
responseHeaders http.Header
// responseBody keeps reference to original ReadCloser.
// TeeReader in copyMiddleware copies read bytes from
// response body (read by SDK) to the buffer. In case
// SDK doesns't read everything readAll method reads from
// this closer to makes sure whole response body is in the buffer.
responseBody io.ReadCloser
// responseReceived flag is used to determine if AI Bridge needs to write custom error:
// - If responseReceived is true, the upstream response is forwarded as-is.
// - If responseReceived is false, no response was returned and there is nothing to forward (eg. connection/client error). Custom error will be returned.
responseReceived atomic.Bool
}
func (r *responseCopier) copyMiddleware(req *http.Request, next option.MiddlewareNext) (*http.Response, error) {
resp, err := next(req)
if err != nil || resp == nil {
return resp, err
}
r.responseReceived.Store(true)
r.responseStatus = resp.StatusCode
r.responseHeaders = resp.Header
resp.Body = io.NopCloser(io.TeeReader(resp.Body, &r.buff))
r.responseBody = resp.Body
return resp, nil
}
// readAll reads all data from resp.Body returned by so TeeReader
// so it appends all read data to the buffer and returns buffer contents.
func (r *responseCopier) readAll() ([]byte, error) {
if r.responseBody == nil {
return []byte{}, nil
}
_, err := io.ReadAll(r.responseBody)
return r.buff.readDelta(), err
}
// forwardResp writes whole response as received to ResponseWriter
func (r *responseCopier) forwardResp(w http.ResponseWriter) error {
// no response was received, nothing to forward
if !r.responseReceived.Load() {
return nil
}
w.Header().Set("Content-Type", r.responseHeaders.Get("Content-Type"))
w.WriteHeader(r.responseStatus)
b, err := r.readAll()
if err != nil {
return xerrors.Errorf("failed to read response body: %w", err)
}
if _, err := w.Write(b); err != nil {
return xerrors.Errorf("failed to write response body: %w", err)
}
return nil
}
// deltaBuffer is a thread safe byte buffer
// supports reading incremental data (added after last read)
type deltaBuffer struct {
mu sync.Mutex
buf bytes.Buffer
}
func (d *deltaBuffer) Write(p []byte) (int, error) {
d.mu.Lock()
defer d.mu.Unlock()
return d.buf.Write(p)
}
// readDelta returns only the bytes appended
// after the last readDelta call.
func (d *deltaBuffer) readDelta() []byte {
d.mu.Lock()
defer d.mu.Unlock()
b := bytes.Clone(d.buf.Bytes())
d.buf.Reset()
return b
}
+384
View File
@@ -0,0 +1,384 @@
package responses //nolint:testpackage // tests unexported internals
import (
"net/http"
"testing"
"time"
"github.com/google/uuid"
oairesponses "github.com/openai/openai-go/v3/responses"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/internal/testutil"
"github.com/coder/coder/v2/aibridge/recorder"
)
func TestRecordPrompt(t *testing.T) {
t.Parallel()
tests := []struct {
name string
promptWasRecorded bool
prompt string
responseID string
wantRecorded bool
wantPrompt string
}{
{
name: "records_prompt_successfully",
prompt: "tell me a joke",
responseID: "resp_123",
wantRecorded: true,
wantPrompt: "tell me a joke",
},
{
name: "records_empty_prompt_successfully",
prompt: "",
responseID: "resp_123",
wantRecorded: true,
wantPrompt: "",
},
{
name: "skips_recording_on_empty_response_id",
prompt: "tell me a joke",
responseID: "",
wantRecorded: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
rec := &testutil.MockRecorder{}
id := uuid.New()
base := &responsesInterceptionBase{
id: id,
recorder: rec,
logger: slog.Make(),
}
base.recordUserPrompt(t.Context(), tc.responseID, tc.prompt)
prompts := rec.RecordedPromptUsages()
if tc.wantRecorded {
require.Len(t, prompts, 1)
require.Equal(t, id.String(), prompts[0].InterceptionID)
require.Equal(t, tc.responseID, prompts[0].MsgID)
require.Equal(t, tc.wantPrompt, prompts[0].Prompt)
} else {
require.Empty(t, prompts)
}
})
}
}
func TestRecordToolUsage(t *testing.T) {
t.Parallel()
id := uuid.MustParse("11111111-1111-1111-1111-111111111111")
tests := []struct {
name string
response *oairesponses.Response
expected []*recorder.ToolUsageRecord
}{
{
name: "nil_response",
response: nil,
expected: nil,
},
{
name: "empty_output",
response: &oairesponses.Response{
ID: "resp_123",
},
expected: nil,
},
{
name: "empty_tool_args",
response: &oairesponses.Response{
ID: "resp_456",
Output: []oairesponses.ResponseOutputItemUnion{
{
Type: "function_call",
CallID: "call_abc",
Name: "get_weather",
Arguments: "",
},
},
},
expected: []*recorder.ToolUsageRecord{
{
InterceptionID: id.String(),
MsgID: "resp_456",
ToolCallID: "call_abc",
Tool: "get_weather",
Args: "",
Injected: false,
},
},
},
{
name: "multiple_tool_calls",
response: &oairesponses.Response{
ID: "resp_789",
Output: []oairesponses.ResponseOutputItemUnion{
{
Type: "function_call",
CallID: "call_1",
Name: "get_weather",
Arguments: `{"location": "NYC"}`,
},
{
Type: "function_call",
CallID: "call_2",
Name: "bad_json_args",
Arguments: `{"bad": args`,
},
{
Type: "message",
ID: "msg_1",
Role: "assistant",
},
{
Type: "custom_tool_call",
CallID: "call_3",
Name: "search",
Input: `{\"query\": \"test\"}`,
},
{
Type: "function_call",
CallID: "call_4",
Name: "calculate",
Arguments: `{"a": 1, "b": 2}`,
},
},
},
expected: []*recorder.ToolUsageRecord{
{
InterceptionID: id.String(),
MsgID: "resp_789",
ToolCallID: "call_1",
Tool: "get_weather",
Args: map[string]any{"location": "NYC"},
Injected: false,
},
{
InterceptionID: id.String(),
MsgID: "resp_789",
ToolCallID: "call_2",
Tool: "bad_json_args",
Args: `{"bad": args`,
Injected: false,
},
{
InterceptionID: id.String(),
MsgID: "resp_789",
ToolCallID: "call_3",
Tool: "search",
Args: `{\"query\": \"test\"}`,
Injected: false,
},
{
InterceptionID: id.String(),
MsgID: "resp_789",
ToolCallID: "call_4",
Tool: "calculate",
Args: map[string]any{"a": float64(1), "b": float64(2)},
Injected: false,
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
rec := &testutil.MockRecorder{}
base := &responsesInterceptionBase{
id: id,
recorder: rec,
logger: slog.Make(),
}
base.recordNonInjectedToolUsage(t.Context(), tc.response)
tools := rec.RecordedToolUsages()
require.Len(t, tools, len(tc.expected))
for i, got := range tools {
got.CreatedAt = time.Time{}
require.Equal(t, tc.expected[i], got)
}
})
}
}
func TestParseJSONArgs(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw string
expected recorder.ToolArgs
}{
{
name: "empty_string",
raw: "",
expected: "",
},
{
name: "whitespace_only",
raw: " \t\n ",
expected: "",
},
{
name: "invalid_json",
raw: "{not valid json}",
expected: "{not valid json}",
},
{
name: "nested_object_with_trailing_spaces",
raw: ` {"user": {"name": "alice", "settings": {"theme": "dark", "notifications": true}}, "count": 42} `,
expected: map[string]any{
"user": map[string]any{
"name": "alice",
"settings": map[string]any{
"theme": "dark",
"notifications": true,
},
},
"count": float64(42),
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
base := &responsesInterceptionBase{}
result := base.parseFunctionCallJSONArgs(t.Context(), tc.raw)
require.Equal(t, tc.expected, result)
})
}
}
func TestRecordTokenUsage(t *testing.T) {
t.Parallel()
id := uuid.MustParse("22222222-2222-2222-2222-222222222222")
tests := []struct {
name string
response *oairesponses.Response
expected *recorder.TokenUsageRecord
}{
{
name: "nil_response",
response: nil,
expected: nil,
},
{
name: "with_all_token_details",
response: &oairesponses.Response{
ID: "resp_full",
Usage: oairesponses.ResponseUsage{
InputTokens: 10,
OutputTokens: 20,
TotalTokens: 30,
InputTokensDetails: oairesponses.ResponseUsageInputTokensDetails{
CachedTokens: 5,
},
OutputTokensDetails: oairesponses.ResponseUsageOutputTokensDetails{
ReasoningTokens: 5,
},
},
},
expected: &recorder.TokenUsageRecord{
InterceptionID: id.String(),
MsgID: "resp_full",
Input: 5, // 10 input - 5 cached
Output: 20,
CacheReadInputTokens: 5,
ExtraTokenTypes: map[string]int64{
"input_cached": 5,
"output_reasoning": 5,
"total_tokens": 30,
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
rec := &testutil.MockRecorder{}
base := &responsesInterceptionBase{
id: id,
recorder: rec,
logger: slog.Make(),
}
base.recordTokenUsage(t.Context(), tc.response)
tokens := rec.RecordedTokenUsages()
if tc.expected == nil {
require.Empty(t, tokens)
} else {
require.Len(t, tokens, 1)
got := tokens[0]
got.CreatedAt = time.Time{} // ignore time
require.Equal(t, tc.expected, got)
}
})
}
}
type mockResponseWriter struct {
headerCalled bool
writeCalled bool
writeHeaderCalled bool
}
func (mrw *mockResponseWriter) Header() http.Header {
mrw.headerCalled = true
return http.Header{}
}
func (mrw *mockResponseWriter) Write([]byte) (int, error) {
mrw.writeCalled = true
return 0, nil
}
func (mrw *mockResponseWriter) WriteHeader(statusCode int) {
mrw.writeHeaderCalled = true
}
func TestResponseCopierDoesntSendIfNoResponseReceived(t *testing.T) {
t.Parallel()
mrw := mockResponseWriter{}
respCopy := responseCopier{}
body := "test_body"
_, _ = respCopy.buff.Write([]byte(body)) // bytes.Buffer.Write never fails
err := respCopy.forwardResp(&mrw)
require.NoError(t, err)
require.False(t, mrw.headerCalled)
require.False(t, mrw.writeCalled)
require.False(t, mrw.writeHeaderCalled)
// after response is received data is forwarded
respCopy.responseReceived.Store(true)
err = respCopy.forwardResp(&mrw)
require.NoError(t, err)
require.True(t, mrw.headerCalled)
require.True(t, mrw.writeCalled)
require.True(t, mrw.writeHeaderCalled)
}
+144
View File
@@ -0,0 +1,144 @@
package responses
import (
"context"
"errors"
"net/http"
"time"
"github.com/google/uuid"
"github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/responses"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/config"
aibcontext "github.com/coder/coder/v2/aibridge/context"
"github.com/coder/coder/v2/aibridge/intercept"
"github.com/coder/coder/v2/aibridge/mcp"
"github.com/coder/coder/v2/aibridge/recorder"
"github.com/coder/coder/v2/aibridge/tracing"
)
type BlockingResponsesInterceptor struct {
responsesInterceptionBase
}
func NewBlockingInterceptor(
id uuid.UUID,
reqPayload RequestPayload,
providerName string,
cfg config.OpenAI,
clientHeaders http.Header,
authHeaderName string,
tracer trace.Tracer,
cred intercept.CredentialInfo,
) *BlockingResponsesInterceptor {
return &BlockingResponsesInterceptor{
responsesInterceptionBase: responsesInterceptionBase{
id: id,
providerName: providerName,
reqPayload: reqPayload,
cfg: cfg,
clientHeaders: clientHeaders,
authHeaderName: authHeaderName,
tracer: tracer,
credential: cred,
},
}
}
func (i *BlockingResponsesInterceptor) Setup(logger slog.Logger, rec recorder.Recorder, mcpProxy mcp.ServerProxier) {
i.responsesInterceptionBase.Setup(logger.Named("blocking"), rec, mcpProxy)
}
func (*BlockingResponsesInterceptor) Streaming() bool {
return false
}
func (i *BlockingResponsesInterceptor) TraceAttributes(r *http.Request) []attribute.KeyValue {
return i.responsesInterceptionBase.baseTraceAttributes(r, false)
}
func (i *BlockingResponsesInterceptor) ProcessRequest(w http.ResponseWriter, r *http.Request) (outErr error) {
ctx, span := i.tracer.Start(r.Context(), "Intercept.ProcessRequest", trace.WithAttributes(tracing.InterceptionAttributesFromContext(r.Context())...))
defer tracing.EndSpanErr(span, &outErr)
if err := i.validateRequest(ctx, w); err != nil {
return err
}
i.injectTools()
var (
response *responses.Response
upstreamErr error
respCopy responseCopier
firstResponseID string
)
prompt, promptFound, err := i.reqPayload.lastUserPrompt(ctx, i.logger)
if err != nil {
i.logger.Warn(ctx, "failed to get user prompt", slog.Error(err))
}
shouldLoop := true
for shouldLoop {
srv := i.newResponsesService()
respCopy = responseCopier{}
opts := i.requestOptions(&respCopy)
opts = append(opts, option.WithRequestTimeout(time.Second*600))
// TODO(ssncferreira): inject actor headers directly in the client-header
// middleware instead of using SDK options.
if actor := aibcontext.ActorFromContext(r.Context()); actor != nil && i.cfg.SendActorHeaders {
opts = append(opts, intercept.ActorHeadersAsOpenAIOpts(actor)...)
}
response, upstreamErr = i.newResponse(ctx, srv, opts)
if upstreamErr != nil || response == nil {
break
}
if firstResponseID == "" {
firstResponseID = response.ID
}
i.recordTokenUsage(ctx, response)
i.recordModelThoughts(ctx, response)
// Check if there any injected tools to invoke.
pending := i.getPendingInjectedToolCalls(response)
shouldLoop, err = i.handleInnerAgenticLoop(ctx, pending, response)
if err != nil {
i.sendCustomErr(ctx, w, http.StatusInternalServerError, err)
shouldLoop = false
}
}
if promptFound {
i.recordUserPrompt(ctx, firstResponseID, prompt)
}
i.recordNonInjectedToolUsage(ctx, response)
if upstreamErr != nil && !respCopy.responseReceived.Load() {
// no response received from upstream, return custom error
i.sendCustomErr(ctx, w, http.StatusInternalServerError, upstreamErr)
return xerrors.Errorf("failed to connect to upstream: %w", upstreamErr)
}
err = respCopy.forwardResp(w)
return errors.Join(upstreamErr, err)
}
func (i *BlockingResponsesInterceptor) newResponse(ctx context.Context, srv responses.ResponseService, opts []option.RequestOption) (_ *responses.Response, outErr error) {
ctx, span := i.tracer.Start(ctx, "Intercept.ProcessRequest.Upstream", trace.WithAttributes(tracing.InterceptionAttributesFromContext(ctx)...))
defer tracing.EndSpanErr(span, &outErr)
// The body is overridden by option.WithRequestBody(reqPayload) in requestOptions
return srv.New(ctx, responses.ResponseNewParams{}, opts...)
}
@@ -0,0 +1,268 @@
package responses
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
"github.com/openai/openai-go/v3/shared/constant"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/recorder"
)
func (i *responsesInterceptionBase) injectTools() {
if i.mcpProxy == nil || !i.hasInjectableTools() {
return
}
i.disableParallelToolCalls()
// Inject tools.
var injected []responses.ToolUnionParam
for _, tool := range i.mcpProxy.ListTools() {
var params map[string]any
if tool.Params != nil {
params = map[string]any{
"type": "object",
"properties": tool.Params,
// "additionalProperties": false, // Only relevant when strict=true.
}
}
// Otherwise the request fails with "None is not of type 'array'" if a nil slice is given.
if len(tool.Required) > 0 {
// Must list ALL properties when strict=true.
params["required"] = tool.Required
}
injected = append(injected, responses.ToolUnionParam{
OfFunction: &responses.FunctionToolParam{
Name: tool.ID,
Strict: openai.Bool(false), // TODO: configurable.
Description: openai.String(tool.Description),
Parameters: params,
},
})
}
updated, err := i.reqPayload.injectTools(injected)
if err != nil {
i.logger.Warn(context.Background(), "failed to inject tools", slog.Error(err))
return
}
i.reqPayload = updated
}
// disableParallelToolCalls disables parallel tool calls, to simplify the inner agentic loop.
// This is best-effort, and failing to set this flag does not fail the request.
// TODO: implement parallel tool calls.
func (i *responsesInterceptionBase) disableParallelToolCalls() {
updated, err := i.reqPayload.disableParallelToolCalls()
if err != nil {
i.logger.Warn(context.Background(), "failed to disable parallel_tool_calls", slog.Error(err))
return
}
i.reqPayload = updated
}
// handleInnerAgenticLoop orchestrates the inner agentic loop whereby injected tools
// are invoked and their results are sent back to the model.
// This is in contrast to regular tool calls which will be handled by the client
// in its own agentic loop.
func (i *responsesInterceptionBase) handleInnerAgenticLoop(ctx context.Context, pending []responses.ResponseFunctionToolCall, response *responses.Response) (bool, error) {
// Invoke any injected function calls.
// The Responses API refers to what we call "tools" as "functions", so we keep the terminology
// consistent in this package.
// See https://platform.openai.com/docs/guides/function-calling
results, err := i.handleInjectedToolCalls(ctx, pending, response)
if err != nil {
return false, xerrors.Errorf("failed to handle injected tool calls: %w", err)
}
// No tool results means no tools were invocable, so the flow is complete.
if len(results) == 0 {
return false, nil
}
// We'll use the tool results to issue another request to provide the model with.
err = i.prepareRequestForAgenticLoop(ctx, response, results)
return true, err
}
// handleInjectedToolCalls checks for function calls that we need to handle in our inner agentic loop.
// These are functions injected by the MCP proxy.
// Returns a list of tool call results.
func (i *responsesInterceptionBase) handleInjectedToolCalls(ctx context.Context, pending []responses.ResponseFunctionToolCall, response *responses.Response) ([]responses.ResponseInputItemUnionParam, error) {
if response == nil {
return nil, xerrors.New("empty response")
}
// MCP proxy has not been configured; no way to handle injected functions.
if i.mcpProxy == nil {
return nil, nil
}
var results []responses.ResponseInputItemUnionParam
for _, fc := range pending {
results = append(results, i.invokeInjectedTool(ctx, response.ID, fc))
}
return results, nil
}
// prepareRequestForAgenticLoop prepares the request by setting the output of the given
// response as input to the next request, in order for the tool call result(s) to make function correctly.
func (i *responsesInterceptionBase) prepareRequestForAgenticLoop(ctx context.Context, response *responses.Response, toolResults []responses.ResponseInputItemUnionParam) error {
// Collect new items to add: response outputs converted to input format + tool results.
var newItems []responses.ResponseInputItemUnionParam
// OutputText is also available, but by definition the trigger for a function call is not a simple
// text response from the model.
for _, output := range response.Output {
if inputItem := i.convertOutputToInput(output); inputItem != nil {
newItems = append(newItems, *inputItem)
}
}
newItems = append(newItems, toolResults...)
updated, err := i.reqPayload.appendInputItems(newItems)
if err != nil {
i.logger.Error(ctx, "failed to rewrite input in inner agentic loop", slog.Error(err))
return xerrors.Errorf("failed to rewrite input: %w", err)
}
i.reqPayload = updated
return nil
}
// getPendingInjectedToolCalls extracts function calls from the response that are managed by MCP proxy.
func (i *responsesInterceptionBase) getPendingInjectedToolCalls(response *responses.Response) []responses.ResponseFunctionToolCall {
var calls []responses.ResponseFunctionToolCall
for _, item := range response.Output {
if item.Type != string(constant.ValueOf[constant.FunctionCall]()) {
continue
}
// Injected functions are defined by MCP, and MCP tools have to have a schema
// for their inputs. The Responses API also supports "Custom Tools":
// https://platform.openai.com/docs/guides/function-calling#custom-tools
// These are like regular functions but their inputs are not schematized.
// As such, custom tools are not considered here.
fc := item.AsFunctionCall()
// Check if this is a tool managed by our MCP proxy
if i.mcpProxy != nil && i.mcpProxy.GetTool(fc.Name) != nil {
calls = append(calls, fc)
}
}
return calls
}
func (i *responsesInterceptionBase) invokeInjectedTool(ctx context.Context, responseID string, fc responses.ResponseFunctionToolCall) responses.ResponseInputItemUnionParam {
tool := i.mcpProxy.GetTool(fc.Name)
if tool == nil {
return responses.ResponseInputItemParamOfFunctionCallOutput(fc.CallID, fmt.Sprintf("error: unknown injected function %q", fc.ID))
}
args := i.parseFunctionCallJSONArgs(ctx, fc.Arguments)
res, err := tool.Call(ctx, args, i.tracer)
_ = i.recorder.RecordToolUsage(ctx, &recorder.ToolUsageRecord{
InterceptionID: i.ID().String(),
MsgID: responseID,
ToolCallID: fc.CallID,
ServerURL: &tool.ServerURL,
Tool: tool.Name,
Args: args,
Injected: true,
InvocationError: err,
})
var output string
if err != nil {
// Results have no fixed structure; if an error occurs, we can just pass back the error.
// https://platform.openai.com/docs/guides/function-calling?strict-mode=enabled#formatting-results
output = fmt.Sprintf("invocation error: %q", err.Error())
} else {
var out strings.Builder
if encErr := json.NewEncoder(&out).Encode(res); encErr != nil {
i.logger.Warn(ctx, "failed to encode tool response", slog.Error(encErr))
output = fmt.Sprintf("result encode error: %q", encErr.Error())
} else {
output = out.String()
}
}
return responses.ResponseInputItemParamOfFunctionCallOutput(fc.CallID, output)
}
// convertOutputToInput converts a response output item to an input item and appends it to the
// request's input list. This is used in agentic loops where we need to feed the model's output
// back as input for the next iteration (e.g., when processing tool call results).
//
// The conversion uses the openai-go library's ToParam() methods where available, which leverage
// param.Override() with raw JSON to preserve all fields. For types without ToParam(), we use
// the ResponseInputItemParamOf* helper functions.
func (i *responsesInterceptionBase) convertOutputToInput(item responses.ResponseOutputItemUnion) *responses.ResponseInputItemUnionParam {
var inputItem responses.ResponseInputItemUnionParam
switch item.Type {
case string(constant.ValueOf[constant.Message]()):
p := item.AsMessage().ToParam()
inputItem = responses.ResponseInputItemUnionParam{OfOutputMessage: &p}
case string(constant.ValueOf[constant.FileSearchCall]()):
p := item.AsFileSearchCall().ToParam()
inputItem = responses.ResponseInputItemUnionParam{OfFileSearchCall: &p}
case string(constant.ValueOf[constant.FunctionCall]()):
p := item.AsFunctionCall().ToParam()
inputItem = responses.ResponseInputItemUnionParam{OfFunctionCall: &p}
case string(constant.ValueOf[constant.WebSearchCall]()):
p := item.AsWebSearchCall().ToParam()
inputItem = responses.ResponseInputItemUnionParam{OfWebSearchCall: &p}
case "computer_call": // No constant.ComputerCall type exists
p := item.AsComputerCall().ToParam()
inputItem = responses.ResponseInputItemUnionParam{OfComputerCall: &p}
case string(constant.ValueOf[constant.Reasoning]()):
p := item.AsReasoning().ToParam()
inputItem = responses.ResponseInputItemUnionParam{OfReasoning: &p}
case string(constant.ValueOf[constant.Compaction]()):
c := item.AsCompaction()
inputItem = responses.ResponseInputItemParamOfCompaction(c.EncryptedContent)
case string(constant.ValueOf[constant.ImageGenerationCall]()):
c := item.AsImageGenerationCall()
inputItem = responses.ResponseInputItemParamOfImageGenerationCall(c.ID, c.Result, c.Status)
case string(constant.ValueOf[constant.CodeInterpreterCall]()):
p := item.AsCodeInterpreterCall().ToParam()
inputItem = responses.ResponseInputItemUnionParam{OfCodeInterpreterCall: &p}
case "custom_tool_call": // No constant.CustomToolCall type exists
p := item.AsCustomToolCall().ToParam()
inputItem = responses.ResponseInputItemUnionParam{OfCustomToolCall: &p}
// Output-only types that don't have direct input equivalents or are handled separately:
// - local_shell_call, shell_call, shell_call_output: Shell tool outputs
// - apply_patch_call, apply_patch_call_output: Apply patch outputs
// - mcp_call, mcp_list_tools, mcp_approval_request: MCP-specific outputs
default:
i.logger.Debug(context.Background(), "skipping output item type for input", slog.F("type", item.Type))
return nil
}
return &inputItem
}
+262
View File
@@ -0,0 +1,262 @@
package responses
import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
"github.com/openai/openai-go/v3/responses"
"github.com/openai/openai-go/v3/shared/constant"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
)
const (
reqPathBackground = "background"
reqPathCallID = "call_id"
reqPathRole = "role"
reqPathInput = "input"
reqPathParallelToolCalls = "parallel_tool_calls"
reqPathStream = "stream"
reqPathTools = "tools"
)
var (
constFunctionCallOutput = string(constant.ValueOf[constant.FunctionCallOutput]())
constInputText = string(constant.ValueOf[constant.InputText]())
constUser = string(constant.ValueOf[constant.User]())
reqPathContent = string(constant.ValueOf[constant.Content]())
reqPathModel = string(constant.ValueOf[constant.Model]())
reqPathText = string(constant.ValueOf[constant.Text]())
reqPathType = string(constant.ValueOf[constant.Type]())
)
// RequestPayload is raw JSON bytes of a Responses API request.
// Methods provide package-specific reads and rewrites while preserving the
// original body for upstream pass-through.
// Note: No changes are made on schema error.
type RequestPayload []byte
func NewRequestPayload(raw []byte) (RequestPayload, error) {
if len(bytes.TrimSpace(raw)) == 0 {
return nil, xerrors.New("empty request body")
}
if !json.Valid(raw) {
return nil, xerrors.New("invalid JSON payload")
}
return RequestPayload(raw), nil
}
func (p RequestPayload) Stream() bool {
return gjson.GetBytes(p, reqPathStream).Bool()
}
func (p RequestPayload) model() string {
return gjson.GetBytes(p, reqPathModel).String()
}
func (p RequestPayload) background() bool {
return gjson.GetBytes(p, reqPathBackground).Bool()
}
func (p RequestPayload) correlatingToolCallID() *string {
items := gjson.GetBytes(p, reqPathInput)
if !items.IsArray() {
return nil
}
arr := items.Array()
if len(arr) == 0 {
return nil
}
last := arr[len(arr)-1]
if last.Get(reqPathType).String() != constFunctionCallOutput {
return nil
}
callID := last.Get(reqPathCallID).String()
if callID == "" {
return nil
}
return &callID
}
// LastUserPrompt returns input text with the "user" role from the last input
// item, or the string input value if present. If no prompt is found, it returns
// empty string, false, nil. Unexpected shapes are treated as unsupported and do
// not fail the request path.
func (p RequestPayload) lastUserPrompt(ctx context.Context, logger slog.Logger) (string, bool, error) {
inputItems := gjson.GetBytes(p, reqPathInput)
if !inputItems.Exists() || inputItems.Type == gjson.Null {
return "", false, nil
}
// 'input' can be either a string or an array of input items:
// https://platform.openai.com/docs/api-reference/responses/create#responses_create-input
// String variant: treat the whole input as the user prompt.
if inputItems.Type == gjson.String {
return inputItems.String(), true, nil
}
// Array variant: checking only the last input item
if !inputItems.IsArray() {
return "", false, xerrors.Errorf("unexpected input type: %s", inputItems.Type)
}
inputItemsArr := inputItems.Array()
if len(inputItemsArr) == 0 {
return "", false, nil
}
lastItem := inputItemsArr[len(inputItemsArr)-1]
if lastItem.Get(reqPathRole).Str != constUser {
// Request was likely not initiated by a prompt but is an iteration of agentic loop.
return "", false, nil
}
// Message content can be either a string or an array of typed content items:
// https://platform.openai.com/docs/api-reference/responses/create#responses_create-input-input_item_list-input_message-content
content := lastItem.Get(reqPathContent)
if !content.Exists() || content.Type == gjson.Null {
return "", false, nil
}
// String variant: use it directly as the prompt.
if content.Type == gjson.String {
return content.Str, true, nil
}
if !content.IsArray() {
return "", false, xerrors.Errorf("unexpected input content type: %s", content.Type)
}
var sb strings.Builder
promptExists := false
for _, c := range content.Array() {
// Ignore non-text content blocks such as images or files.
if c.Get(reqPathType).Str != constInputText {
continue
}
text := c.Get(reqPathText)
if text.Type != gjson.String {
logger.Warn(ctx, fmt.Sprintf("unexpected input content array element text type: %v", text.Type))
continue
}
if promptExists {
_ = sb.WriteByte('\n') // strings.Builder.WriteByte never fails
}
promptExists = true
_, _ = sb.WriteString(text.Str) // strings.Builder.WriteString never fails
}
if !promptExists {
return "", false, nil
}
return sb.String(), true, nil
}
func (p RequestPayload) injectTools(injected []responses.ToolUnionParam) (RequestPayload, error) {
if len(injected) == 0 {
return p, nil
}
existing, err := p.toolItems()
if err != nil {
return p, xerrors.Errorf("failed to get existing tools: %w", err)
}
allTools := make([]any, 0, len(existing)+len(injected))
for _, item := range existing {
allTools = append(allTools, item)
}
for _, tool := range injected {
allTools = append(allTools, tool)
}
return p.set(reqPathTools, allTools)
}
func (p RequestPayload) disableParallelToolCalls() (RequestPayload, error) {
return p.set(reqPathParallelToolCalls, false)
}
func (p RequestPayload) appendInputItems(items []responses.ResponseInputItemUnionParam) (RequestPayload, error) {
if len(items) == 0 {
return p, nil
}
existing, err := p.inputItems()
if err != nil {
return p, xerrors.Errorf("failed to get existing 'input' items: %w", err)
}
allInput := make([]any, 0, len(existing)+len(items))
allInput = append(allInput, existing...)
for _, item := range items {
allInput = append(allInput, item)
}
return p.set(reqPathInput, allInput)
}
func (p RequestPayload) inputItems() ([]any, error) {
input := gjson.GetBytes(p, reqPathInput)
if !input.Exists() || input.Type == gjson.Null {
return []any{}, nil
}
if input.Type == gjson.String {
return []any{responses.ResponseInputItemParamOfMessage(input.String(), responses.EasyInputMessageRoleUser)}, nil
}
if !input.IsArray() {
return nil, xerrors.Errorf("unsupported 'input' type: %s", input.Type)
}
items := input.Array()
existing := make([]any, 0, len(items))
for _, item := range items {
existing = append(existing, json.RawMessage(item.Raw))
}
return existing, nil
}
func (p RequestPayload) toolItems() ([]json.RawMessage, error) {
tools := gjson.GetBytes(p, reqPathTools)
if !tools.Exists() {
return nil, nil
}
if !tools.IsArray() {
return nil, xerrors.Errorf("unsupported 'tools' type: %s", tools.Type)
}
items := tools.Array()
existing := make([]json.RawMessage, 0, len(items))
for _, item := range items {
existing = append(existing, json.RawMessage(item.Raw))
}
return existing, nil
}
func (p RequestPayload) set(path string, value any) (RequestPayload, error) {
updated, err := sjson.SetBytes(p, path, value)
if err != nil {
return p, xerrors.Errorf("failed to set value at path %s: %w", path, err)
}
return updated, nil
}
@@ -0,0 +1,527 @@
package responses //nolint:testpackage // tests unexported internals
import (
"encoding/json"
"fmt"
"testing"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/aibridge/fixtures"
"github.com/coder/coder/v2/aibridge/utils"
)
func TestNewRequestPayload(t *testing.T) {
t.Parallel()
payloadWithWrongTypes := []byte(`{"model":123,"stream":"yes","input":42,"background":"nope"}`)
tests := []struct {
name string
raw []byte
want []byte
model string
stream bool
background bool
err string
}{
{
name: "empty payload",
raw: nil,
want: nil,
err: "empty request body",
},
{
name: "invalid json",
raw: []byte(`{broken`),
want: nil,
err: "invalid JSON payload",
},
{
// RequestPayload just checks for JSON validity,
// schema errors are not surfaced here and
// the original body is preserved for upstream handling
// similar to how reverse proxy would behave.
name: "wrong field types still wrap",
raw: payloadWithWrongTypes,
want: payloadWithWrongTypes,
model: "123",
stream: false,
background: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
payload, err := NewRequestPayload(tc.raw)
if tc.err != "" {
require.ErrorContains(t, err, tc.err)
assert.Nil(t, payload)
return
}
require.NoError(t, err)
require.NotNil(t, payload)
assert.EqualValues(t, tc.want, payload)
assert.Equal(t, tc.model, payload.model())
assert.Equal(t, tc.stream, payload.Stream())
assert.Equal(t, tc.background, payload.background())
})
}
}
func TestCorrelatingToolCallID(t *testing.T) {
t.Parallel()
tests := []struct {
name string
payload []byte
wantCall *string
}{
{
name: "no input items",
payload: []byte(`{"model":"gpt-4o"}`),
},
{
name: "empty input array",
payload: []byte(`{"model":"gpt-4o","input":[]}`),
},
{
name: "no function_call_output items",
payload: []byte(`{"model":"gpt-4o","input":[{"role":"user","content":"hi"}]}`),
},
{
name: "single function_call_output",
payload: []byte(`{"model":"gpt-4o","input":[{"role":"user","content":"hi"},{"type":"function_call_output","call_id":"call_abc","output":"result"}]}`),
wantCall: utils.PtrTo("call_abc"),
},
{
name: "multiple function_call_outputs returns last",
payload: []byte(`{"model":"gpt-4o","input":[{"type":"function_call_output","call_id":"call_first","output":"r1"},{"role":"user","content":"hi"},{"type":"function_call_output","call_id":"call_second","output":"r2"}]}`),
wantCall: utils.PtrTo("call_second"),
},
{
name: "last input is not a tool result",
payload: []byte(`{"model":"gpt-4o","input":[{"type":"function_call_output","call_id":"call_first","output":"r1"},{"role":"user","content":"hi"}]}`),
},
{
name: "missing call id",
payload: []byte(`{"input":[{"type":"function_call_output","output":"ok"}]}`),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
callID := mustPayload(t, tc.payload).correlatingToolCallID()
assert.Equal(t, tc.wantCall, callID)
})
}
}
func TestLastUserPrompt(t *testing.T) {
t.Parallel()
tests := []struct {
name string
reqPayload []byte
expect string
found bool
expectErr string
}{
{
name: "no input",
reqPayload: []byte(`{}`),
found: false,
},
{
name: "input null",
reqPayload: []byte(`{"input": null}`),
found: false,
},
{
name: "empty input array",
reqPayload: []byte(`{"input": []}`),
found: false,
},
{
name: "input empty string",
reqPayload: []byte(`{"input": ""}`),
expect: "",
found: true,
},
{
name: "input array content empty string",
reqPayload: []byte(`{"input": [{"role": "user", "content": ""}]}`),
expect: "",
found: true,
},
{
name: "input array content array empty string",
reqPayload: []byte(`{"input": [ { "role": "user", "content": [{"type": "input_text", "text": ""}] } ] }`),
expect: "",
found: true,
},
{
name: "input array content array multiple inputs",
reqPayload: []byte(`{"input": [ { "role": "user", "content": [{"type": "input_text", "text": "a"}, {"type": "input_text", "text": "b"}] } ] }`),
expect: "a\nb",
found: true,
},
{
name: "simple string input",
reqPayload: fixtures.Request(t, fixtures.OaiResponsesBlockingSimple),
expect: "tell me a joke",
found: true,
},
{
name: "array single input string",
reqPayload: fixtures.Request(t, fixtures.OaiResponsesBlockingSingleBuiltinTool),
expect: "Is 3 + 5 a prime number? Use the add function to calculate the sum.",
found: true,
},
{
name: "array multiple items content objects",
reqPayload: fixtures.Request(t, fixtures.OaiResponsesStreamingCodex),
expect: "hello",
found: true,
},
{
name: "input integer",
reqPayload: []byte(`{"input": 123}`),
expectErr: "unexpected input type",
},
{
name: "no user role",
reqPayload: []byte(`{"input": [{"role": "assistant", "content": "hello"}]}`),
found: false,
},
{
name: "user with empty content array",
reqPayload: []byte(`{"input": [{"role": "user", "content": []}]}`),
found: false,
},
{
name: "user content missing",
reqPayload: []byte(`{"input": [{"role": "user"}]}`),
found: false,
},
{
name: "user content null",
reqPayload: []byte(`{"input": [{"role": "user", "content": null}]}`),
found: false,
},
{
name: "input array integer",
reqPayload: []byte(`{"input": [{"role": "user", "content": 123}]}`),
expectErr: "unexpected input content type",
},
{
name: "user with non input_text content",
reqPayload: []byte(`{"input": [{"role": "user", "content": [{"type": "input_image", "url": "http://example.com/img.png"}]}]}`),
found: false,
},
{
name: "user content not last",
reqPayload: []byte(`{"input": [ {"role": "user", "content":"input"}, {"role": "assistant", "content": "hello"} ]}`),
found: false,
},
{
name: "input array content array integer",
reqPayload: []byte(`{"input": [ { "role": "user", "content": [{"type": "input_text", "text": 123}] } ] }`),
found: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
prompt, promptFound, err := mustPayload(t, tc.reqPayload).lastUserPrompt(t.Context(), slog.Make())
if tc.expectErr != "" {
require.ErrorContains(t, err, tc.expectErr)
return
}
require.NoError(t, err)
require.Equal(t, tc.expect, prompt)
require.Equal(t, tc.found, promptFound)
})
}
}
func TestInjectTools(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw []byte
injected []responses.ToolUnionParam
wantNames []string
wantErr string
wantSame bool
}{
{
name: "appends to existing tools",
raw: []byte(`{"model":"gpt-4o","input":"hello","tools":[{"type":"function","name":"existing"}]}`),
injected: []responses.ToolUnionParam{injectedFunctionTool("injected")},
wantNames: []string{"existing", "injected"},
},
{
name: "adds tools when none exist",
raw: []byte(`{"model":"gpt-4o","input":"hello"}`),
injected: []responses.ToolUnionParam{injectedFunctionTool("injected")},
wantNames: []string{"injected"},
},
{
name: "adds to empty tools array",
raw: []byte(`{"model":"gpt-4o","input":"hello","tools":[]}`),
injected: []responses.ToolUnionParam{injectedFunctionTool("injected")},
wantNames: []string{"injected"},
},
{
name: "appends multiple injected tools",
raw: []byte(`{"model":"gpt-4o","input":"hello","tools":[{"type":"function","name":"existing"}]}`),
injected: []responses.ToolUnionParam{
injectedFunctionTool("injected-one"),
injectedFunctionTool("injected-two"),
},
wantNames: []string{"existing", "injected-one", "injected-two"},
},
{
name: "empty injected tools is no op",
raw: []byte(`{"model":"gpt-4o","input":"hello","tools":[{"type":"function","name":"existing"}]}`),
wantSame: true,
},
{
name: "errors on unsupported tools shape",
raw: []byte(`{"model":"gpt-4o","input":"hello","tools":"bad"}`),
injected: []responses.ToolUnionParam{injectedFunctionTool("injected")},
wantErr: "failed to get existing tools: unsupported 'tools' type: String",
wantSame: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := mustPayload(t, tc.raw)
updated, err := p.injectTools(tc.injected)
if tc.wantErr != "" {
require.EqualError(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
if tc.wantSame {
require.EqualValues(t, tc.raw, updated)
}
for i, wantName := range tc.wantNames {
path := fmt.Sprintf("tools.%d.name", i) // name of the i-th element in tools array
require.Equal(t, wantName, gjson.GetBytes(updated, path).String())
}
})
}
}
func TestDisableParallelToolCalls(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw []byte
}{
{
name: "sets flag when not present",
raw: []byte(`{"model":"gpt-4o"}`),
},
{
name: "overrides when already true",
raw: []byte(`{"model":"gpt-4o","parallel_tool_calls":true}`),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := mustPayload(t, tc.raw)
updated, err := p.disableParallelToolCalls()
require.NoError(t, err)
assert.False(t, gjson.GetBytes(updated, "parallel_tool_calls").Bool())
})
}
}
func TestAppendInputItems(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw []byte
items []responses.ResponseInputItemUnionParam
wantErr string
wantSame bool
wantPaths map[string]string
}{
{
name: "string input becomes user message",
raw: []byte(`{"model":"gpt-4o","input":"hello"}`),
items: []responses.ResponseInputItemUnionParam{responses.ResponseInputItemParamOfFunctionCallOutput("call_123", "done")},
wantPaths: map[string]string{
"input.0.role": "user",
"input.0.content": "hello",
"input.1.type": "function_call_output",
"input.1.call_id": "call_123",
},
},
{
name: "array input is preserved and appended",
raw: []byte(`{"model":"gpt-4o","input":[{"role":"user","content":"hello"}]}`),
items: []responses.ResponseInputItemUnionParam{responses.ResponseInputItemParamOfFunctionCallOutput("call_123", "done")},
wantPaths: map[string]string{
"input.0.content": "hello",
"input.1.call_id": "call_123",
},
},
{
name: "unsupported input shape errors during rewrite",
raw: []byte(`{"model":"gpt-4o","input":123}`),
items: []responses.ResponseInputItemUnionParam{responses.ResponseInputItemParamOfFunctionCallOutput("call_123", "done")},
wantErr: "failed to get existing 'input' items: unsupported 'input' type: Number",
wantSame: true,
},
{
name: "missing input creates appended input",
raw: []byte(`{"model":"gpt-4o"}`),
items: []responses.ResponseInputItemUnionParam{responses.ResponseInputItemParamOfFunctionCallOutput("call_123", "done")},
wantPaths: map[string]string{
"input.0.type": "function_call_output",
"input.0.call_id": "call_123",
},
},
{
name: "null input creates appended input",
raw: []byte(`{"model":"gpt-4o","input":null}`),
items: []responses.ResponseInputItemUnionParam{responses.ResponseInputItemParamOfFunctionCallOutput("call_123", "done")},
wantPaths: map[string]string{
"input.0.type": "function_call_output",
"input.0.call_id": "call_123",
},
},
{
name: "multiple output item types are appended in order",
raw: []byte(`{"model":"gpt-4o","input":[{"role":"user","content":"hello"}]}`),
items: []responses.ResponseInputItemUnionParam{
responses.ResponseInputItemParamOfCompaction("encrypted-content"),
responses.ResponseInputItemParamOfOutputMessage([]responses.ResponseOutputMessageContentUnionParam{
{
OfOutputText: &responses.ResponseOutputTextParam{
Annotations: []responses.ResponseOutputTextAnnotationUnionParam{},
Text: "assistant text",
},
},
}, "msg_123", responses.ResponseOutputMessageStatusCompleted),
responses.ResponseInputItemParamOfFileSearchCall("fs_123", []string{"hello"}, "completed"),
responses.ResponseInputItemParamOfImageGenerationCall("img_123", "base64-image", "completed"),
},
wantPaths: map[string]string{
"input.0.content": "hello",
"input.1.type": "compaction",
"input.2.type": "message",
"input.2.id": "msg_123",
"input.2.content.0.type": "output_text",
"input.2.content.0.text": "assistant text",
"input.3.type": "file_search_call",
"input.3.id": "fs_123",
"input.4.type": "image_generation_call",
"input.4.id": "img_123",
},
},
{
name: "empty appended items is no op",
raw: []byte(`{"model":"gpt-4o","input":"hello"}`),
wantSame: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := mustPayload(t, tc.raw)
updated, err := p.appendInputItems(tc.items)
if tc.wantErr != "" {
require.EqualError(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
if tc.wantSame {
require.EqualValues(t, tc.raw, updated)
}
for path, want := range tc.wantPaths {
require.Equal(t, want, gjson.GetBytes(updated, path).String())
}
})
}
}
func TestChainedRewritesProduceValidJSON(t *testing.T) {
t.Parallel()
p := mustPayload(t, []byte(`{"model":"gpt-4o","input":"hello"}`))
p, err := p.injectTools([]responses.ToolUnionParam{{
OfFunction: &responses.FunctionToolParam{
Name: "tool_a",
Description: openai.String("tool"),
Strict: openai.Bool(false),
Parameters: map[string]any{
"type": "object",
},
},
}})
require.NoError(t, err)
p, err = p.disableParallelToolCalls()
require.NoError(t, err)
p, err = p.appendInputItems([]responses.ResponseInputItemUnionParam{
responses.ResponseInputItemParamOfFunctionCallOutput("call_123", "done"),
})
require.NoError(t, err)
assert.True(t, json.Valid(p), "chained rewrites should produce valid JSON")
assert.Equal(t, "tool_a", gjson.GetBytes(p, "tools.0.name").String())
assert.Equal(t, "call_123", gjson.GetBytes(p, "input.1.call_id").String())
assert.False(t, gjson.GetBytes(p, "parallel_tool_calls").Bool())
}
func injectedFunctionTool(name string) responses.ToolUnionParam {
return responses.ToolUnionParam{
OfFunction: &responses.FunctionToolParam{
Name: name,
Description: openai.String("tool"),
Strict: openai.Bool(false),
Parameters: map[string]any{
"type": "object",
},
},
}
}
func mustPayload(t *testing.T, raw []byte) RequestPayload {
t.Helper()
payload, err := NewRequestPayload(raw)
require.NoError(t, err)
return payload
}

Some files were not shown because too many files have changed in this diff Show More