Compare commits
241 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3ca654ae4 | |||
| 8b69f05cc0 | |||
| da365455cc | |||
| 24c454a3d4 | |||
| 450957a64a | |||
| 2e6fdf2344 | |||
| 3d139c1a24 | |||
| f957981c8b | |||
| 584c61acb5 | |||
| f95a5202bf | |||
| d954460380 | |||
| f4240bb8c1 | |||
| 7caef4987f | |||
| 9b91af8ab7 | |||
| 506fba9ebf | |||
| 461a31e5d8 | |||
| e3a0dcd6fc | |||
| 12ada0115f | |||
| 7b0421d8c6 | |||
| 477d6d0cde | |||
| de61ac529d | |||
| 7f496c2f18 | |||
| 590235138f | |||
| 543c448b72 | |||
| 35c26ce22a | |||
| c2592c9f12 | |||
| b969d66978 | |||
| 1f808cdc62 | |||
| 497f637f58 | |||
| be686a8d0d | |||
| 7b7baea851 | |||
| a3de0fc78d | |||
| ab77154975 | |||
| c5d720f73d | |||
| 983819860f | |||
| f820945d9f | |||
| da5395a8ae | |||
| 86b919e4f7 | |||
| 233343c010 | |||
| 3a612898c6 | |||
| 3f7a3e3354 | |||
| 17a71aea72 | |||
| 7d3c5ac78c | |||
| d87c5ef439 | |||
| ef3e17317c | |||
| 1187b84c54 | |||
| 45336bd9ce | |||
| 36cf7debce | |||
| 027c222e82 | |||
| d00f148b76 | |||
| 48bc215f20 | |||
| 08bd9e672a | |||
| c5f1a2fccf | |||
| 655d647d40 | |||
| f3f0a2c553 | |||
| 5453a6c6d6 | |||
| 21c08a37d7 | |||
| 2bd261fbbf | |||
| cffc68df58 | |||
| 6e5335df1e | |||
| 16265e834e | |||
| 565a15bc9b | |||
| 76a2cb1af5 | |||
| 684f21740d | |||
| 86ca61d6ca | |||
| f0521cfa3c | |||
| 0c5d189aff | |||
| d7c8213eee | |||
| 63924ac687 | |||
| 6c47e9ea23 | |||
| aede045549 | |||
| 2ea08aa168 | |||
| d4b9248202 | |||
| fd6c623560 | |||
| 99da498679 | |||
| a20b817c28 | |||
| d5a1792f07 | |||
| beb99c17de | |||
| 8913f9f5c1 | |||
| acd5f01b4b | |||
| 6c62d8f5e6 | |||
| 5000f15021 | |||
| 44be5a0d1e | |||
| 3ca2aae9ca | |||
| 01080302a5 | |||
| 61d6c728b9 | |||
| 648787e739 | |||
| d2950e7615 | |||
| df8f695e84 | |||
| 8bb48ffdda | |||
| 4cfbf544a0 | |||
| a2ce74f398 | |||
| 0060dee222 | |||
| 5ff1058f30 | |||
| 500fc5e2a4 | |||
| baba9e6ede | |||
| b36619b905 | |||
| 937f50f0ae | |||
| a16755dd66 | |||
| 8bdc35f91f | |||
| 5b32c4d79d | |||
| 8625543413 | |||
| e18094825a | |||
| 919dc299fc | |||
| 7e63fe68f7 | |||
| a1d51f0dab | |||
| 333503f74e | |||
| 01b8cdb00d | |||
| ec83065b59 | |||
| 2a1bef18e0 | |||
| 8369fa88fd | |||
| da3c46b557 | |||
| 53482adc2d | |||
| aa0e288b88 | |||
| 1c4a9ed745 | |||
| 5b6b7719df | |||
| 990c006f28 | |||
| 0cb942aab2 | |||
| b0a6802d12 | |||
| 8d08885792 | |||
| f68161350a | |||
| 9ac67a5253 | |||
| 7d0a0c6495 | |||
| 17dec2a70f | |||
| f796f3645f | |||
| d5ed51a190 | |||
| 796e8e4e18 | |||
| 5b28548d1c | |||
| b5da77ff55 | |||
| cc143c8990 | |||
| 3def04a3ee | |||
| d4a9c63e91 | |||
| 00217fefa5 | |||
| fce05d0428 | |||
| 2ebc076b9e | |||
| e71dc6dd4d | |||
| ca3ae3643d | |||
| ed5c06f039 | |||
| 1221622bf0 | |||
| aa5ec0bfcc | |||
| 16add93908 | |||
| fe13fd065c | |||
| fb788530b3 | |||
| f4dc8f6b11 | |||
| bbeff0d4b5 | |||
| 78fa8094cc | |||
| a85e00eed0 | |||
| da50a34414 | |||
| cd784c755a | |||
| eb4860aac3 | |||
| 07fbe8ca7d | |||
| ba0a64d483 | |||
| 7757cd8e08 | |||
| fc1e0beb3b | |||
| 3a4a0b7270 | |||
| 11c1afb5e9 | |||
| be2e641162 | |||
| 83e2699914 | |||
| 515ba209fd | |||
| d15bfc2cb0 | |||
| 308053b0e4 | |||
| 7c29355e84 | |||
| 7c048d8eb4 | |||
| e81275a91c | |||
| 4a363b0d85 | |||
| 7dc81bdef1 | |||
| ee855f9618 | |||
| b1c42bb630 | |||
| c048a4093e | |||
| 1cc23a3144 | |||
| dee5ec51c0 | |||
| e3c59c00cd | |||
| ba734f8b10 | |||
| 28062862a0 | |||
| 129e3509a3 | |||
| 53a1b6d67e | |||
| 8c8b307b97 | |||
| 12f87acad6 | |||
| 7198f9040d | |||
| ddafdbcbce | |||
| 196dc51edf | |||
| 2a51687ff3 | |||
| 19e44f4136 | |||
| 2ea89e1f1b | |||
| faa5db0cf0 | |||
| 3758b02595 | |||
| 7861fcf1f6 | |||
| 4b52656958 | |||
| f5b98aa12d | |||
| 7ddde0887b | |||
| bec426b24f | |||
| d6df78c9b9 | |||
| 19390a5841 | |||
| 2d03f7fd3d | |||
| 153a66b579 | |||
| 5cba59af79 | |||
| 1d16ff1ca6 | |||
| b86161e0a6 | |||
| a164d508cf | |||
| b9f140e53e | |||
| 7f7b13f0ab | |||
| e2bbd12137 | |||
| e769d1bd7d | |||
| cccb680ec2 | |||
| e8fb418820 | |||
| 2c5e003c91 | |||
| f44a8994da | |||
| 84b94a8376 | |||
| 2a990ce758 | |||
| c86f1288f1 | |||
| 9440adf435 | |||
| 755e8be5ad | |||
| c9e335c453 | |||
| 2d1f35f8a6 | |||
| b0036af57b | |||
| 2953245862 | |||
| 5d07014f9f | |||
| 002e88fefc | |||
| bbf3fbc830 | |||
| 9fa103929a | |||
| acd2ff63a7 | |||
| e3e17e15f7 | |||
| af678606fc | |||
| 3190406de3 | |||
| 3ce82bb885 | |||
| 348a3bd693 | |||
| 75f1503b41 | |||
| c33cd19a05 | |||
| adcea865c7 | |||
| 5e3bccd96c | |||
| 3950947c58 | |||
| b3d5b8d13c | |||
| a00afe4b5a | |||
| a5cc579453 | |||
| ef3aade647 | |||
| 3cc31de57a | |||
| d2c308e481 | |||
| 953c3bdc0f | |||
| ca879ffae6 | |||
| 0880a4685b | |||
| 3f8e3007d8 |
@@ -111,8 +111,8 @@ Tier 2 file filters:
|
||||
|
||||
- **Modernization Reviewer**: one instance per language present in the diff. Filter by extension:
|
||||
- Go: `*.go` — reference `.claude/docs/GO.md` before reviewing.
|
||||
- TypeScript: `*.ts` `*.tsx`
|
||||
- React: `*.tsx` `*.jsx`
|
||||
- TypeScript: `*.ts` `*.tsx`: reference `.agents/skills/deep-review/references/typescript.md` before reviewing.
|
||||
- React: `*.tsx` `*.jsx`: reference `.agents/skills/deep-review/references/react.md` before reviewing.
|
||||
|
||||
`.tsx` files match both TypeScript and React filters. Spawn both instances when the diff contains `.tsx` changes — TS covers language-level patterns; React covers component and hooks patterns. Before spawning, verify each instance's filter produces a non-empty diff. Skip instances whose filtered diff is empty.
|
||||
|
||||
@@ -155,9 +155,11 @@ File scope: {filter from step 2}.
|
||||
Output file: {REVIEW_DIR}/{role-name}.md
|
||||
```
|
||||
|
||||
For the Modernization Reviewer (Go), add after the methodology line:
|
||||
For Modernization Reviewer instances, add the language reference after the methodology line:
|
||||
|
||||
> Read `.claude/docs/GO.md` as your Go language reference before reviewing.
|
||||
- **Go:** `Read .claude/docs/GO.md as your Go language reference before reviewing.`
|
||||
- **TypeScript:** `Read .agents/skills/deep-review/references/typescript.md as your TypeScript language reference before reviewing.`
|
||||
- **React:** `Read .agents/skills/deep-review/references/react.md as your React language reference before reviewing.`
|
||||
|
||||
For re-reviews, append to both Tier 1 and Tier 2 prompts:
|
||||
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
# Modern React (18–19.2) + Compiler 1.0 — Reference
|
||||
|
||||
Reference for writing idiomatic React. Covers what changed, what it replaced, and what to reach for. Includes React Compiler patterns — what the compiler handles automatically, what it changes semantically, and how to verify its behavior empirically. Scope: client-side SPA patterns only. Server Components, `use server`, and `use client` directives are framework-specific and omitted. Check the project's React version and compiler config before reaching for newer APIs.
|
||||
|
||||
## How modern React thinks differently
|
||||
|
||||
**Concurrent rendering** (18): React can now pause, interrupt, and resume renders. This is the foundation everything else builds on. Most existing code "just works," but components that produce side effects during render (mutations, subscriptions, network calls in the render body) are unsafe and will misbehave. Concurrent features are opt-in — they only activate when you use a concurrent API like `startTransition` or `useDeferredValue`.
|
||||
|
||||
**Urgent vs. non-urgent updates** (18): The `startTransition` / `useTransition` API introduces a formal split between updates that must feel immediate (typing, clicking) and updates that can be interrupted (filtering a large list, navigating to a new screen). Non-urgent updates yield to urgent ones mid-render. Use this instead of `setTimeout` or manual debounce when you want the UI to stay responsive during expensive re-renders.
|
||||
|
||||
**Actions** (19): Async functions passed to `startTransition` are called "Actions." They automatically manage pending state, error handling, and optimistic updates as a unit. The `useActionState` hook and `<form action={fn}>` prop are built on this. The pattern replaces the hand-rolled `isPending/setIsPending` + `try/catch` + `setError` boilerplate that was previously necessary for every data mutation.
|
||||
|
||||
**Automatic batching** (18): State updates are now batched everywhere — inside `setTimeout`, `Promise.then`, native event handlers, etc. Previously batching only happened inside React-managed event handlers. If you genuinely need a synchronous flush, use `flushSync`.
|
||||
|
||||
**Automatic memoization** (Compiler 1.0): React Compiler is a build-time Babel plugin that automatically inserts memoization into components and hooks. It replaces manual `useMemo`, `useCallback`, and `React.memo` — including conditional memoization and memoization after early returns, which manual APIs cannot express. The compiler only processes components and hooks, not standalone functions. It understands data flow and mutability through its own HIR (High-level Intermediate Representation), so it can memoize more granularly than a human would. Projects adopt it incrementally — typically via path-based Babel overrides or the `"use memo"` directive. Components that violate the Rules of React are silently skipped (no build error), so the automated lint tools that check compiler compatibility matter.
|
||||
|
||||
## Replace these patterns
|
||||
|
||||
The left column reflects patterns common before React 18/19. Write the right column instead. The "Since" column tells you the minimum React version required.
|
||||
|
||||
| Old pattern | Modern replacement | Since |
|
||||
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----- |
|
||||
| `ReactDOM.render(<App />, el)` | `createRoot(el).render(<App />)` | 18 |
|
||||
| `ReactDOM.hydrate(<App />, el)` | `hydrateRoot(el, <App />)` | 18 |
|
||||
| `ReactDOM.unmountComponentAtNode(el)` | `root.unmount()` | 18 |
|
||||
| `ReactDOM.findDOMNode(this)` | DOM ref: `const ref = useRef(); ref.current` | 18 |
|
||||
| `<Context.Provider value={v}>` | `<Context value={v}>` | 19 |
|
||||
| `React.forwardRef((props, ref) => ...)` | `function Comp({ ref, ...props }) { ... }` (ref as a regular prop) | 19 |
|
||||
| String ref `ref="input"` in class components | Callback ref or `createRef()` | 19 |
|
||||
| `Heading.propTypes = { ... }` | TypeScript / ES6 type annotations | 19 |
|
||||
| `Component.defaultProps = { ... }` on function components | ES6 default parameters `({ text = 'Hi' })` | 19 |
|
||||
| Legacy Context: `contextTypes` + `getChildContext` | `React.createContext()` + `contextType` | 19 |
|
||||
| `import { act } from 'react-dom/test-utils'` | `import { act } from 'react'` | 19 |
|
||||
| `import ShallowRenderer from 'react-test-renderer/shallow'` | `import ShallowRenderer from 'react-shallow-renderer'` | 19 |
|
||||
| Manual `isPending` state around async calls | `const [isPending, startTransition] = useTransition()` | 18 |
|
||||
| Manual optimistic state + revert logic | `useOptimistic(currentValue)` | 19 |
|
||||
| `useEffect` to subscribe to external stores | `useSyncExternalStore(subscribe, getSnapshot)` | 18 |
|
||||
| Hand-rolled unique ID (counter, random, index) | `useId()` — SSR-safe, hydration-safe | 18 |
|
||||
| `useEffect` to inject `<title>` or `<meta>` / `react-helmet` | Render `<title>`, `<meta>`, `<link>` directly in components; React hoists them | 19 |
|
||||
| `ReactDOM.useFormState(action, initial)` (Canary name) | `useActionState(action, initial)` | 19 |
|
||||
| `useReducer<React.Reducer<State, Action>>(reducer)` | `useReducer(reducer)` — infers from the reducer function | 19 |
|
||||
| `<div ref={current => (instance = current)} />` (implicit return) | `<div ref={current => { instance = current }} />` (explicit block body) | 19 |
|
||||
| `useRef<T>()` with no argument | `useRef<T>(undefined)` or `useRef<T \| null>(null)` — argument is now required | 19 |
|
||||
| `MutableRefObject<T>` type annotation | `RefObject<T>` — all refs are mutable now; `MutableRefObject` is deprecated | 19 |
|
||||
| `React.createFactory('button')` | `<button />` JSX | 19 |
|
||||
| `useMemo(() => expr, [deps])` in compiled components | `const val = expr;` — compiler memoizes automatically | C 1.0 |
|
||||
| `useCallback(fn, [deps])` in compiled components | `const fn = () => { ... };` — compiler memoizes automatically | C 1.0 |
|
||||
| `React.memo(Component)` in compiled components | Plain component — compiler skips re-render when props are unchanged | C 1.0 |
|
||||
| `eslint-plugin-react-compiler` (standalone) | `eslint-plugin-react-hooks@latest` (compiler rules merged into recommended) | C 1.0 |
|
||||
| `useRef` + `useLayoutEffect` for stable callbacks | `useEffectEvent(fn)` — compiler handles both, but `useEffectEvent` is clearer | 19.2 |
|
||||
|
||||
## New capabilities
|
||||
|
||||
These enable things that weren't practical before. Reach for them in the described situations.
|
||||
|
||||
| What | Since | When to use it |
|
||||
| -------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `useTransition()` / `startTransition()` | 18 | Mark a state update as non-urgent so React can interrupt it to handle clicks or keystrokes. The `isPending` boolean lets you show a loading indicator without blocking the UI. |
|
||||
| `useDeferredValue(value, initialValue?)` | 18 / 19 | Defer re-rendering a slow subtree: pass the deferred value as a prop, wrap the expensive child in `memo`. Unlike debounce, uses no fixed timeout — renders as soon as the browser is idle. The `initialValue` arg (19) avoids a flash on first render. |
|
||||
| `useId()` | 18 | Generate a stable, SSR-consistent ID for accessibility attributes (`htmlFor`, `aria-describedby`). Do not use for list keys. |
|
||||
| `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)` | 18 | Subscribe to external (non-React) state stores safely under concurrent rendering. Preferred over `useEffect`-based subscriptions in libraries. |
|
||||
| `useActionState(action, initialState)` | 19 | Manage an async mutation: returns `[state, wrappedAction, isPending]`. Handles pending, result, and error state as a unit. Replaces the manual `isPending` + `try/catch` + `setError` pattern. |
|
||||
| `useOptimistic(currentValue)` | 19 | Show a speculative value while an async Action is in flight. Returns `[optimisticValue, setOptimistic]`. React automatically reverts to `currentValue` when the transition settles. |
|
||||
| `use(promiseOrContext)` | 19 | Read a promise or Context value inside a component or custom hook. Unlike hooks, `use` can be called conditionally (after early returns). Promises must come from a cache — do not create them during render. |
|
||||
| `useFormStatus()` (from `react-dom`) | 19 | Read `{ pending, data, method, action }` of the nearest parent `<form>` Action. Works across component boundaries without prop drilling — useful for submit buttons inside design-system components. |
|
||||
| `useEffectEvent(fn)` | 19.2 | Extract a non-reactive callback from an effect. The function sees the latest props/state without being listed in deps, and is never stale. Replaces the `useRef`-and-mutate-in-layout-effect workaround for stable event-like callbacks. The compiler has built-in knowledge of this hook and correctly prunes its return value from effect dependency arrays. Both `useEffectEvent` and the old ref workaround compile cleanly; `useEffectEvent` is preferred for clarity. |
|
||||
| `<Activity>` | 19.2 | Hide part of the UI while preserving its state and DOM. React deprioritizes updates to hidden content. Use via framework APIs for route prerendering or tab preservation — not a direct replacement for CSS `visibility`. |
|
||||
| `captureOwnerStack()` | 19.1 | Dev-only API that returns a string showing which components are responsible for rendering the current component (owner stack, not call stack). Useful for custom error overlays. Returns `null` in production. |
|
||||
| `<form action={fn}>` | 19 | Pass an async function as a form's `action` prop. React handles submission, pending state, and automatic form reset on success. Works with `useActionState` and `useFormStatus`. |
|
||||
| Ref cleanup function | 19 | Return a cleanup function from a ref callback: `ref={el => { ...; return () => cleanup(); }}`. React calls it on unmount. Replaces the pattern of checking `el === null` in the callback. |
|
||||
| `<link rel="stylesheet" precedence="default">` | 19 | Declare a stylesheet next to the component that needs it. React deduplicates and inserts it in the correct order before revealing Suspense content. |
|
||||
| `preinit`, `preload`, `prefetchDNS`, `preconnect` (from `react-dom`) | 19 | Imperatively hint the browser to load resources early. Call from render or event handlers. React deduplicates hints across the component tree. |
|
||||
| React Compiler (`babel-plugin-react-compiler`) | C 1.0 | Build-time automatic memoization for components and hooks. Install, add to Babel/Vite pipeline. Projects typically start with path-based overrides to compile a subset of files. |
|
||||
| `"use memo"` directive | C 1.0 | Opt a single function into compilation when using `compilationMode: 'annotation'`. Place at the start of the function body. Module-level `"use memo"` at the top of a file compiles all functions in that file. |
|
||||
| `"use no memo"` directive | C 1.0 | Temporary escape hatch — skip compilation for a specific component or hook that causes a runtime regression. Not a permanent solution. Place at the start of the function body. |
|
||||
| Compiler-powered ESLint rules | C 1.0 | Rules for purity, refs, set-state-in-render, immutability, etc. now ship in `eslint-plugin-react-hooks` recommended preset. Surface Rules-of-React violations even without the compiler installed. Note: some projects use Biome instead — check project lint config. |
|
||||
|
||||
## Key APIs
|
||||
|
||||
### `useTransition` and `startTransition` (18)
|
||||
|
||||
`useTransition` returns `[isPending, startTransition]`. Wrap any state update that is not directly tied to the user's current gesture inside `startTransition`. React will render the old UI while computing the new one, and `isPending` is `true` during that window.
|
||||
|
||||
In React 19, `startTransition` can accept an async function (an "Action"). React sets `isPending` to `true` for the entire duration of the async work, not just during the synchronous part.
|
||||
|
||||
```tsx
|
||||
// 18: synchronous transition
|
||||
const [isPending, startTransition] = useTransition();
|
||||
startTransition(() => setQuery(input));
|
||||
|
||||
// 19: async Action — isPending stays true until the await settles
|
||||
startTransition(async () => {
|
||||
const err = await updateName(name);
|
||||
if (err) setError(err);
|
||||
});
|
||||
```
|
||||
|
||||
Use `startTransition` (the module-level export) when you cannot use the hook (outside a component, in a router callback, etc.).
|
||||
|
||||
### `useDeferredValue` (18 / 19)
|
||||
|
||||
Creates a "lagging" copy of a value. Pass it to a memoized, expensive component so that React can render the stale UI while computing the updated one.
|
||||
|
||||
```tsx
|
||||
// 19: initialValue shows '' on first render; avoids loading flash
|
||||
const deferred = useDeferredValue(searchQuery, "");
|
||||
return <Results query={deferred} />; // Results wrapped in memo
|
||||
```
|
||||
|
||||
`deferred !== searchQuery` while the deferred render is in progress — use this to show a "stale" indicator.
|
||||
|
||||
### `useActionState` (19)
|
||||
|
||||
Replaces the `useState` + `isPending` + `try/catch` + `setError` boilerplate for any async operation that can be retried or submitted as a form.
|
||||
|
||||
```tsx
|
||||
const [error, submitAction, isPending] = useActionState(
|
||||
async (prevState, formData) => {
|
||||
const err = await updateName(formData.get("name"));
|
||||
if (err) return err; // returned value becomes next state
|
||||
redirect("/profile");
|
||||
return null;
|
||||
},
|
||||
null, // initialState
|
||||
);
|
||||
|
||||
// Use submitAction as the form's action prop or call it directly
|
||||
<form action={submitAction}>
|
||||
<input name="name" />
|
||||
<button disabled={isPending}>Save</button>
|
||||
{error && <p>{error}</p>}
|
||||
</form>;
|
||||
```
|
||||
|
||||
### `useOptimistic` (19)
|
||||
|
||||
Shows a speculative value immediately while an async Action is in progress. React automatically reverts to the server-confirmed value when the Action resolves or rejects.
|
||||
|
||||
```tsx
|
||||
const [optimisticName, setOptimisticName] = useOptimistic(currentName);
|
||||
|
||||
const submit = async (formData) => {
|
||||
const newName = formData.get("name");
|
||||
setOptimisticName(newName); // shows immediately
|
||||
await updateName(newName); // reverts if this throws
|
||||
};
|
||||
```
|
||||
|
||||
### `use()` (19)
|
||||
|
||||
Unlike hooks, `use` can appear after conditional statements. Two primary uses:
|
||||
|
||||
**Reading a promise** (must be stable — from a cache, not created inline):
|
||||
|
||||
```tsx
|
||||
function Comments({ commentsPromise }) {
|
||||
const comments = use(commentsPromise); // suspends until resolved
|
||||
return comments.map((c) => <p key={c.id}>{c.text}</p>);
|
||||
}
|
||||
```
|
||||
|
||||
**Reading context after an early return** (hooks cannot appear after `return`):
|
||||
|
||||
```tsx
|
||||
function Heading({ children }) {
|
||||
if (!children) return null;
|
||||
const theme = use(ThemeContext); // valid here; hooks would not be
|
||||
return <h1 style={{ color: theme.color }}>{children}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
### `useSyncExternalStore` (18)
|
||||
|
||||
The correct way for libraries (and app code) to subscribe to non-React state. Prevents tearing under concurrent rendering.
|
||||
|
||||
```tsx
|
||||
const value = useSyncExternalStore(
|
||||
store.subscribe, // called when store changes
|
||||
store.getSnapshot, // returns current value (must be stable reference if unchanged)
|
||||
store.getServerSnapshot, // optional: for SSR
|
||||
);
|
||||
```
|
||||
|
||||
## Verifying compiler behavior
|
||||
|
||||
The compiler is a black box unless you inspect its output. When reviewing code in compiled paths, run the compiler on the specific code to see what it actually does. Do not guess — verify.
|
||||
|
||||
**Run the compiler on a code snippet:**
|
||||
|
||||
```sh
|
||||
cd site && node -e "
|
||||
const {transformSync} = require('@babel/core');
|
||||
const code = \`<paste component here>\`;
|
||||
const diagnostics = [];
|
||||
const result = transformSync(code, {
|
||||
plugins: [
|
||||
['@babel/plugin-syntax-typescript', {isTSX: true}],
|
||||
['babel-plugin-react-compiler', {
|
||||
logger: {
|
||||
logEvent(_, event) {
|
||||
if (event.kind === 'CompileError' || event.kind === 'CompileSkip') {
|
||||
diagnostics.push(event.detail?.toString?.()?.substring(0, 200));
|
||||
}
|
||||
},
|
||||
},
|
||||
}],
|
||||
],
|
||||
filename: 'test.tsx',
|
||||
});
|
||||
console.log('Compiled:', result.code.includes('_c('));
|
||||
if (diagnostics.length) console.log('Diagnostics:', diagnostics);
|
||||
console.log(result.code);
|
||||
"
|
||||
```
|
||||
|
||||
**Reading compiled output:**
|
||||
|
||||
- `const $ = _c(N)` — allocates N memoization cache slots.
|
||||
- `if ($[n] !== dep)` — cache invalidation guard. Re-computes when `dep` changes (referential equality).
|
||||
- `if ($[n] === Symbol.for("react.memo_cache_sentinel"))` — one-time initialization. Runs once on first render, cached forever after. This is how the compiler handles expressions with no reactive dependencies.
|
||||
- `_temp` functions — pure callbacks the compiler hoisted out of the component body.
|
||||
|
||||
**Check all compiled files at once:**
|
||||
|
||||
```sh
|
||||
cd site && pnpm run lint:compiler
|
||||
```
|
||||
|
||||
This runs the compiler on every file in the compiled paths and reports CompileError / CompileSkip diagnostics. Zero diagnostics means all functions compiled cleanly.
|
||||
|
||||
**What the compiler catches vs. what it does not:**
|
||||
|
||||
The compiler emits `CompileError` for mutations of props, state, or hook arguments during render, and for `ref.current` access during render. The project's lint pipeline catches these automatically — do not flag them in review.
|
||||
|
||||
The compiler does **not** flag impure function calls during render (`Math.random()`, `Date.now()`, `new Date()`). Instead it silently memoizes them with a sentinel guard, freezing the value after first render. This changes semantics without any diagnostic. Verify suspicious calls by running the compiler and checking for sentinel guards in the output.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
Things that are easy to get wrong even when you know the modern API exists. Check your output against these.
|
||||
|
||||
**Effects run twice in development with StrictMode.** React 18 intentionally mounts → unmounts → remounts every component in dev to surface effects that are not resilient to remounting. This is not a bug. If an effect breaks on the second mount, it is missing a cleanup function. Write `return () => cleanup()` from every effect that sets up a subscription, timer, or external resource.
|
||||
|
||||
**Concurrent rendering can call render multiple times.** The render function (component body) may be called more than once before React commits to the DOM. Side effects (mutations, subscriptions, logging) in the render body will run multiple times. Move them into `useEffect` or event handlers.
|
||||
|
||||
**Do not create promises during render and pass them to `use()`.** A new promise is created every render, causing an infinite suspend-retry loop. Create the promise outside the component (module level), or use a caching library (SWR, React Query, `cache()` from React) to stabilize it.
|
||||
|
||||
**`useOptimistic` reverts automatically — do not fight it.** The optimistic value is a presentation layer only. When the Action settles, React replaces it with the real `currentValue` you passed in. Do not try to sync optimistic state back to your real state; let React handle the revert.
|
||||
|
||||
**`flushSync` opts out of automatic batching.** If third-party code or a browser API (e.g. `ResizeObserver`) calls `setState` and you need synchronous DOM flushing, wrap with `flushSync(() => setState(...))`. This is a last resort; prefer letting React batch.
|
||||
|
||||
**`forwardRef` still works in React 19 but will be deprecated.** Function components accept `ref` as a plain prop now. New code should use the prop directly. Existing `forwardRef` wrappers continue to work without changes; migrate when convenient.
|
||||
|
||||
**`<Activity>` does not unmount.** Content inside a hidden `<Activity>` boundary stays mounted. Effects keep running. Use it for preserving scroll position or form state, not for preventing expensive mounts — use lazy loading for that.
|
||||
|
||||
**TypeScript: implicit returns from ref callbacks are now type errors.** In React 19, returning anything other than a cleanup function (or nothing) from a ref callback is rejected by the TypeScript types. The most common case is arrow-function refs that implicitly return the DOM node:
|
||||
|
||||
```tsx
|
||||
// Error in React 19 types:
|
||||
<div ref={el => (instance = el)} />
|
||||
|
||||
// Fix — use a block body:
|
||||
<div ref={el => { instance = el; }} />
|
||||
```
|
||||
|
||||
**TypeScript: `useRef` now requires an argument.** `useRef<T>()` with no argument is a type error. Pass `undefined` for mutable refs or `null` for DOM refs you initialize on mount: `useRef<T>(undefined)` / `useRef<HTMLDivElement | null>(null)`.
|
||||
|
||||
**`useId` output format changed across versions.** React 18 produced `:r0:`. React 19.1 changed it to `«r0»`. React 19.2 changed it again to `_r0`. Do not parse or depend on the specific format — treat it as an opaque string.
|
||||
|
||||
**`useFormStatus` reads the nearest parent `<form>` with a function `action`.** It does not reflect native HTML form submissions — only React Actions. A submit button that is a sibling of `<form>` (rather than a descendant) will not see the form's status.
|
||||
|
||||
**Context as a provider (`<Context>`) requires React 19; `<Context.Provider>` still works.** Do not use `<Context>` shorthand in a codebase that needs to support React 18. The two forms can coexist during migration.
|
||||
|
||||
**Compiler freezes impure expressions silently.** `Math.random()`, `Date.now()`, `new Date()`, and `window.innerWidth` in a component body all compile without diagnostics. The compiler wraps them in a sentinel guard (`Symbol.for("react.memo_cache_sentinel")`) that runs the expression once and caches the result forever. The value never updates on re-render. Fix: move to a `useState` initializer (`useState(() => Math.random())`), `useEffect`, or event handler.
|
||||
|
||||
**Component granularity affects compiler optimization.** When one pattern in a component causes a `CompileError` (e.g., a necessary `ref.current` read during render), the compiler skips the **entire** component. If the rest of the component would benefit from compilation, extract the non-compilable pattern into a small child component. This keeps the parent compiled.
|
||||
|
||||
**The compiler only memoizes components and hooks.** Standalone utility functions (even expensive ones called during render) are not compiled. If a utility function is truly expensive, it still needs its own caching strategy outside of React (e.g., a module-level cache, `WeakMap`, etc.).
|
||||
|
||||
**Changing memoization can shift `useEffect` firing.** A value that was unstable before compilation may become stable after, causing an effect that depended on it to fire less often. Conversely, future compiler changes may alter memoization granularity. Effects that use memoized values as dependencies should be resilient to these changes — they should be true synchronization effects, not "run this when X changes" hacks.
|
||||
|
||||
## Behavioral changes that affect code
|
||||
|
||||
- **Automatic batching** (18): State updates in `setTimeout`, `Promise.then`, `addEventListener` callbacks, etc. are now batched into a single re-render. Previously only React synthetic event handlers were batched. Code that relied on unbatched updates (reading DOM synchronously after each `setState`) must use `flushSync`.
|
||||
|
||||
- **StrictMode double-invoke** (18): In development, every component is mounted → unmounted → remounted with the previous state. Every effect runs cleanup → setup twice on initial mount. `useMemo` and `useCallback` also double-invoke their functions. Production behavior is unchanged. If a test or component breaks under this, the component had a latent cleanup bug.
|
||||
|
||||
- **StrictMode ref double-invoke** (19): In development, ref callbacks are also invoked twice on mount (attach → detach → attach). Return a cleanup function from the ref callback to handle detach correctly.
|
||||
|
||||
- **StrictMode memoization reuse** (19): During the second pass of double-rendering, `useMemo` and `useCallback` now reuse the cached result from the first pass instead of calling the function again. Components that are already StrictMode-compatible should not notice a difference.
|
||||
|
||||
- **Suspense fallback commits immediately** (19): When a component suspends, React now commits the nearest `<Suspense>` fallback without waiting for sibling trees to finish rendering. After the fallback is shown, React "pre-warms" suspended siblings in the background. This makes fallbacks appear faster but changes the order of rendering work.
|
||||
|
||||
- **Error re-throwing removed** (19): Errors that are not caught by an Error Boundary are now reported to `window.reportError` (not re-thrown). Errors caught by an Error Boundary go to `console.error` once. If your production monitoring relied on the re-thrown error, add handlers to `createRoot`: `createRoot(el, { onUncaughtError, onCaughtError })`.
|
||||
|
||||
- **Transitions in `popstate` are synchronous** (19): Browser back/forward navigation triggers synchronous transition flushing. This ensures the URL and UI update together atomically during history navigation.
|
||||
|
||||
- **`useEffect` from discrete events flushes synchronously** (18): Effects triggered by a click or keydown (discrete events) are now flushed synchronously before the browser paints, consistent with `useLayoutEffect` for those cases.
|
||||
|
||||
- **Hydration mismatches treated as errors** (18 / improved in 19): Text content mismatches between server HTML and client render revert to client rendering up to the nearest `<Suspense>` boundary. React 19 logs a single diff instead of multiple warnings, making mismatches much easier to diagnose.
|
||||
|
||||
- **New JSX transform required** (19): The automatic JSX runtime introduced in 2020 (`react/jsx-runtime`) is now mandatory. The classic transform (which required `import React from 'react'` in every file) is no longer supported. Most toolchains have already shipped the new transform; check your Babel or TypeScript config if you see warnings.
|
||||
|
||||
- **UMD builds removed** (19): React no longer ships UMD bundles. Load via npm and a bundler, or use an ESM CDN (`import React from "https://esm.sh/react@19"`).
|
||||
|
||||
- **React Compiler automatic memoization** (Compiler 1.0): Build-time Babel plugin that inserts memoization into components and hooks. Components that follow the Rules of React are automatically memoized; components that violate them are silently skipped (no build error, no runtime change). The compiler can memoize conditionally and after early returns — things impossible with manual `useMemo`/`useCallback`. Works with React 17+ via `react-compiler-runtime`; best with React 19+. Projects adopt incrementally via path-based Babel overrides, `compilationMode: 'annotation'`, or the `"use memo"` / `"use no memo"` directives. Check the project's Vite/Babel config to know which paths are compiled. Compiled components show a "Memo ✨" badge in React DevTools.
|
||||
@@ -0,0 +1,199 @@
|
||||
# Modern TypeScript (5.0–6.0 RC) — Reference
|
||||
|
||||
Reference for writing idiomatic TypeScript. Covers what changed, what it replaced, and what to reach for. Respect the project's minimum TypeScript version: don't emit features from a version newer than what the project targets. Check `package.json` and `tsconfig.json` before writing code.
|
||||
|
||||
## How modern TypeScript thinks differently
|
||||
|
||||
The 5.x era resolves years of module system ambiguity and cleans house on legacy options. Three themes dominate:
|
||||
|
||||
**Module semantics are explicit.** `--verbatimModuleSyntax` (5.0) makes import/export intent visible in source: type imports must carry `type`, value imports stay. Combined with `--module preserve` or `--moduleResolution bundler`, the compiler now accurately models what bundlers and modern runtimes actually do. `import defer` (5.9) extends the model to deferred evaluation.
|
||||
|
||||
**Resource lifetimes are first-class.** `using` and `await using` (5.2) provide deterministic cleanup without `try/finally`. Any object implementing `Symbol.dispose` participates. `DisposableStack` handles ad-hoc multi-resource cleanup in functions where creating a full class is overkill.
|
||||
|
||||
**Inference is smarter about what it knows.** Inferred type predicates (5.5) let `.filter(x => x !== undefined)` produce `T[]` instead of `(T | undefined)[]` automatically. `NoInfer<T>` (5.4) gives library authors precise control over which parameters drive inference. Narrowing now survives closures after last assignment, constant indexed accesses, and `switch (true)` patterns.
|
||||
|
||||
**TypeScript 6.0 is a transition release toward 7.0** (the Go-native port). It turns years of soft deprecations into errors and changes several defaults. Most impactful: `types` defaults to `[]` (must list `@types` packages explicitly), `rootDir` defaults to `.`, `strict` defaults to `true`, `module` defaults to `esnext`. Projects relying on implicit behavior need explicit config. Check the deprecations section before upgrading.
|
||||
|
||||
## Replace these patterns
|
||||
|
||||
The left column reflects patterns still common before TypeScript 5.x. Write the right column instead. The "Since" column tells you the minimum TypeScript version required.
|
||||
|
||||
| Old pattern | Modern replacement | Since |
|
||||
| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------ |
|
||||
| `--experimentalDecorators` + legacy decorator signatures | Standard decorators (TC39): `function dec(target, context: ClassMethodDecoratorContext)` — no flag needed | 5.0 |
|
||||
| Requiring callers to add `as const` at call sites | `<const T extends HasNames>(arg: T)` — `const` modifier on type parameter | 5.0 |
|
||||
| `--importsNotUsedAsValues` + `--preserveValueImports` | `--verbatimModuleSyntax` | 5.0 |
|
||||
| `import { Foo } from "..."` when `Foo` is only used as a type | `import { type Foo } from "..."` or `import type { Foo } from "..."` | 5.0 |
|
||||
| `"extends": "@tsconfig/strictest/tsconfig.json"` chain | `"extends": ["@tsconfig/strictest/tsconfig.json", "./tsconfig.base.json"]` (array form) | 5.0 |
|
||||
| `try { ... } finally { resource.close(); resource.delete(); }` | `using resource = acquireResource()` — calls `[Symbol.dispose]()` automatically | 5.2 |
|
||||
| `try { ... } finally { await resource.close() }` | `await using resource = acquireAsyncResource()` | 5.2 |
|
||||
| Ad-hoc cleanup with multiple `try/finally` blocks | `using cleanup = new DisposableStack(); cleanup.defer(() => ...)` | 5.2 |
|
||||
| `import data from "./data.json" assert { type: "json" }` | `import data from "./data.json" with { type: "json" }` | 5.3 |
|
||||
| `.filter(Boolean)` or `.filter(x => !!x)` to remove nulls | `.filter(x => x !== undefined)` or `.filter(x => x !== null)` (infers type predicate) | 5.5 |
|
||||
| Extra phantom type param to block inference bleed: `<C extends string, D extends C>` | `NoInfer<C>` on the parameter you don't want to drive inference | 5.4 |
|
||||
| `/** @typedef {import("./types").Foo} Foo */` in JS files | `/** @import { Foo } from "./types" */` (JSDoc `@import` tag) | 5.5 |
|
||||
| `myArray.reverse()` mutating in place | `myArray.toReversed()` (returns new array) | 5.2 |
|
||||
| `myArray.sort(cmp)` mutating in place | `myArray.toSorted(cmp)` (returns new array) | 5.2 |
|
||||
| `const copy = [...arr]; copy[i] = v` | `arr.with(i, v)` (returns new array) | 5.2 |
|
||||
| Manual `has`/`get`/`set` pattern on `Map` | `map.getOrInsert(key, defaultValue)` or `getOrInsertComputed(key, fn)` | 6.0 RC |
|
||||
| `new RegExp(str.replace(/[.\*+?^${}() | [\]\\]/g, '\\$&'))` | `new RegExp(RegExp.escape(str))` | 6.0 RC |
|
||||
| `--moduleResolution node` (node10) | `--moduleResolution nodenext` (Node.js) or `--moduleResolution bundler` (bundlers/Bun) | 6.0 RC |
|
||||
| `"baseUrl": "./src"` + `"@app/*": ["app/*"]` in paths | Remove `baseUrl`; use `"@app/*": ["./src/app/*"]` in paths directly | 6.0 RC |
|
||||
| `module Foo { export const x = 1; }` | `namespace Foo { export const x = 1; }` | 6.0 RC |
|
||||
| `export * from "..."` when all re-exported members are types | `export type * from "..."` (or `export type * as ns from "..."`) | 5.0 |
|
||||
| `function f(): undefined { return undefined; }` — explicit return required in `: undefined`-returning function | Remove the `return` entirely; `undefined`-returning functions no longer require any return statement | 5.1 |
|
||||
| Manual type predicate annotation on a simple arrow: `(x: T \| undefined): x is T => x !== undefined` | Remove the annotation; TypeScript infers `x is T` from `!== null/undefined` and `instanceof` checks automatically | 5.5 |
|
||||
| `const val = obj[key]; if (typeof val === "string") { use(val); }` — extract to const to narrow indexed access | `if (typeof obj[key] === "string") { obj[key].toUpperCase(); }` directly — both `obj` and `key` must be effectively constant | 5.5 |
|
||||
| Copy narrowed `let`/param to a `const`, or restructure code to escape stale closure narrowing after reassignment | Remove the copy; narrowing survives into closures created after the last assignment to the variable | 5.4 |
|
||||
| `(arr as string[]).filter(...)` or restructure to avoid "not callable" errors on `string[] \| number[]` | Call `.filter`, `.find`, `.some`, `.every`, `.reduce` directly on union-of-array types | 5.2 |
|
||||
| `if`/`else` chain used to work around lack of narrowing inside a `switch (true)` body | `switch (true)` — each `case` condition now narrows the tested variable in its clause | 5.3 |
|
||||
|
||||
## New capabilities
|
||||
|
||||
These enable things that weren't practical before. Reach for them in the described situations.
|
||||
|
||||
| What | Since | When to use it |
|
||||
| ----------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `using` / `await using` declarations | 5.2 | Any resource needing deterministic cleanup (file handles, DB connections, locks, event listeners). Object must implement `Symbol.dispose` / `Symbol.asyncDispose`. |
|
||||
| `DisposableStack` / `AsyncDisposableStack` | 5.2 | Ad-hoc multi-resource cleanup without creating a class. Call `.defer(fn)` right after acquiring each resource. Stack disposes in LIFO order. |
|
||||
| `const` modifier on type parameters | 5.0 | Force `const`-like (literal/readonly tuple) inference at call sites without requiring callers to write `as const`. Constraint must use `readonly` arrays. |
|
||||
| Decorator metadata (`Symbol.metadata`) | 5.2 | Attach and read per-class metadata from decorators via `context.metadata`. Retrieved as `MyClass[Symbol.metadata]`. Requires `Symbol.metadata ??= Symbol(...)` polyfill. |
|
||||
| `NoInfer<T>` utility type | 5.4 | Prevent a parameter from contributing inference candidates for `T`. Use when one argument should be the "source of truth" and others should only be checked against it. |
|
||||
| Inferred type predicates | 5.5 | Filter callbacks that test for `!== null` or `instanceof` now automatically produce a type predicate. `Array.prototype.filter` then narrows the result array type. |
|
||||
| `--isolatedDeclarations` | 5.5 | Require explicit return types on exported declarations. Unlocks parallel declaration emit by external tooling (esbuild, oxc, etc.) without needing a full type-checker pass. |
|
||||
| `${configDir}` in tsconfig paths | 5.5 | Anchor `typeRoots`, `paths`, `outDir`, etc. in a shared base tsconfig to the _consuming_ project's directory, not the shared file's location. |
|
||||
| Always-truthy/nullish check errors | 5.6 | Catches regex literals in `if`, arrow functions as comparators, `?? 100` on non-nullable left side, misplaced parentheses. No API to call; existing bugs now surface as errors. |
|
||||
| Iterator helper methods (`IteratorObject`) | 5.6 | Built-in iterators from `Map`, `Set`, generators, etc. now have `.map()`, `.filter()`, `.take()`, `.drop()`, `.flatMap()`, `.toArray()`, `.reduce()`, etc. Use `Iterator.from(iterable)` to wrap any iterable. |
|
||||
| `--noUncheckedSideEffectImports` | 5.6 | Error when a side-effect import (`import "..."`) resolves to nothing. Catches typos in polyfill or CSS imports. |
|
||||
| `--noCheck` | 5.6 | Skip type checking entirely during emit. Useful for separating "fast emit" from "thorough check" pipeline stages, especially with `--isolatedDeclarations`. |
|
||||
| `--rewriteRelativeImportExtensions` | 5.7 | Rewrite `.ts`→`.js`, `.tsx`→`.jsx`, `.mts`→`.mjs`, `.cts`→`.cjs` in relative imports during emit. Required when writing `.ts` imports for Node.js strip-types mode and still needing `.js` output for library distribution. |
|
||||
| `--erasableSyntaxOnly` | 5.8 | Error on constructs that can't be type-stripped by Node.js `--experimental-strip-types`: `enum`, `namespace` with code, parameter properties, `import =` aliases. |
|
||||
| `require()` of ESM under `--module nodenext` | 5.8 | Node.js 22+ allows CJS to `require()` ESM files (no top-level `await`). TypeScript now allows this under `nodenext` without error. |
|
||||
| `import defer * as ns from "..."` | 5.9 | Defer module _evaluation_ (not loading) until first property access. Module is loaded and verified at import time; side-effects are delayed. Only works with `--module preserve` or `esnext`. |
|
||||
| `Set` algebra methods | 5.5 | Non-mutating: `union`, `intersection`, `difference`, `symmetricDifference` → new `Set`. Predicate: `isSubsetOf`, `isSupersetOf`, `isDisjointFrom` → `boolean`. Requires `esnext` or `es2025` lib. |
|
||||
| `Object.groupBy` / `Map.groupBy` | 5.4 | Group an iterable into buckets by key function. Return type has all keys as optional (not every key is guaranteed present). Requires `esnext` or `es2024`+ lib. |
|
||||
| `Temporal` API types | 6.0 RC | `Temporal.Now`, `Temporal.Instant`, `Temporal.PlainDate`, etc. Available under `esnext` or `esnext.temporal` lib. Usable in runtimes that already ship it (V8 118+, SpiderMonkey, etc.). |
|
||||
| `@satisfies` in JSDoc | 5.0 | Validates that a JS expression satisfies a type without widening it — the TS `satisfies` operator for `.js` files. Write `/** @satisfies {MyType} */` above the declaration or inline on a parenthesized expression. |
|
||||
| `@overload` in JSDoc | 5.0 | Declare multiple call signatures for a JS function. Each JSDoc comment tagged `@overload` is treated as a distinct overload; the final JSDoc comment (without `@overload`) describes the implementation signature. |
|
||||
| Getter/setter with completely unrelated types | 5.1 | `get style(): CSSStyleDeclaration` and `set style(v: string)` can now have fully unrelated types, provided both have explicit type annotations. Previously the getter type was required to be a subtype of the setter type. |
|
||||
| `instanceof` narrowing via `Symbol.hasInstance` | 5.3 | When a class defines `static [Symbol.hasInstance](val: unknown): val is T`, the `instanceof` operator now narrows to the predicate type `T`, not the class type itself. Useful when the runtime check and the structural type differ. |
|
||||
| Regex literal syntax checking | 5.5 | TypeScript validates regex literal syntax: malformed groups, nonexistent backreferences, named capture mismatches, and features not available at the current `--target`. No API needed; existing latent bugs surface as errors automatically. |
|
||||
| `--build` continues past intermediate errors | 5.6 | `tsc --build` no longer stops at the first failing project. All projects are built and errors reported together. Use `--stopOnBuildErrors` to restore the old stop-on-first-error behavior. Useful for monorepos during upgrades. |
|
||||
| `--module node18` | 5.8 | Stable `--module` flag for Node.js 18 semantics: disallows `require()` of ESM (unlike `nodenext`) and still allows import assertions. Use when pinned to Node 18 and not ready for `nodenext` behavior changes. |
|
||||
| `--module node20` | 5.9 | Stable `--module` flag for Node.js 20 semantics: permits `require()` of ESM, rejects import assertions. Implies `--target es2023` (unlike `nodenext`, which floats to `esnext`). |
|
||||
|
||||
## Key APIs
|
||||
|
||||
### `Disposable` / `AsyncDisposable` / stacks (5.2)
|
||||
|
||||
Global types provided by TypeScript's lib (requires `esnext.disposable` or `esnext` in `lib`):
|
||||
|
||||
- `Disposable` — `{ [Symbol.dispose](): void }`
|
||||
- `AsyncDisposable` — `{ [Symbol.asyncDispose](): PromiseLike<void> }`
|
||||
- `DisposableStack` — `defer(fn)`, `use(resource)`, `adopt(value, disposeFn)`, `move()`. Is itself `Disposable`.
|
||||
- `AsyncDisposableStack` — async equivalent. Is itself `AsyncDisposable`.
|
||||
- `SuppressedError` — thrown when both the scope body and a `[Symbol.dispose]` throw. `.error` holds the dispose-phase error; `.suppressed` holds the original error.
|
||||
|
||||
Polyfill the symbols in older runtimes:
|
||||
|
||||
```ts
|
||||
Symbol.dispose ??= Symbol("Symbol.dispose");
|
||||
Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose");
|
||||
```
|
||||
|
||||
### Decorator context types (5.0)
|
||||
|
||||
Each decorator kind receives a typed context object as its second parameter:
|
||||
|
||||
- `ClassDecoratorContext`
|
||||
- `ClassMethodDecoratorContext`
|
||||
- `ClassGetterDecoratorContext`
|
||||
- `ClassSetterDecoratorContext`
|
||||
- `ClassFieldDecoratorContext`
|
||||
- `ClassAccessorDecoratorContext`
|
||||
|
||||
All context objects have `.name`, `.kind`, `.static`, `.private`, and `.metadata`. Method/getter/setter/accessor contexts also have `.addInitializer(fn)` for running code at construction time.
|
||||
|
||||
### `IteratorObject` (5.6)
|
||||
|
||||
`IteratorObject<T, TReturn, TNext>` is the new type for built-in iterable iterators. Key methods: `map`, `filter`, `take`, `drop`, `flatMap`, `forEach`, `reduce`, `some`, `every`, `find`, `toArray`. Not the same as the pre-existing structural `Iterator<T>` protocol.
|
||||
|
||||
- Generators produce `Generator<T>` which extends `IteratorObject`.
|
||||
- `Map.prototype.entries()` returns `MapIterator<[K, V]>`, `Set.prototype.values()` returns `SetIterator<T>`, etc.
|
||||
- `Iterator.from(iterable)` converts any `Iterable` to an `IteratorObject`.
|
||||
- `AsyncIteratorObject` exists for async parity.
|
||||
- `--strictBuiltinIteratorReturn` (new `--strict`-mode flag in 5.6) makes the return type of `BuiltinIteratorReturn` be `undefined` instead of `any`, catching unchecked `done` access.
|
||||
|
||||
### Array copying methods (5.2)
|
||||
|
||||
Declared on `Array`, `ReadonlyArray`, and all `TypedArray` types. Use these instead of the mutating variants when you need to preserve the original:
|
||||
|
||||
| Mutating | Non-mutating copy |
|
||||
| ---------------------------------- | ------------------------------------- |
|
||||
| `arr.sort(cmp)` | `arr.toSorted(cmp)` |
|
||||
| `arr.reverse()` | `arr.toReversed()` |
|
||||
| `arr.splice(start, del, ...items)` | `arr.toSpliced(start, del, ...items)` |
|
||||
| `arr[i] = v` | `arr.with(i, v)` |
|
||||
|
||||
## Pitfalls
|
||||
|
||||
Things easy to get wrong even when you know the modern API exists. Check your output against these.
|
||||
|
||||
**tsconfig defaults changed hard in 6.0.** `types: []` means no `@types/*` packages load implicitly. If you see floods of "cannot find name 'process'" or "cannot find module 'fs'" after upgrading to 6.0, add `"types": ["node"]` (or whatever you need) to `compilerOptions`. `rootDir: "."` means a project with source in `src/` will emit to `dist/src/` instead of `dist/` — add `"rootDir": "./src"` explicitly. `strict: true` by default means projects with loose code see new errors.
|
||||
|
||||
**`using` requires a runtime polyfill on older runtimes.** `Symbol.dispose` and `Symbol.asyncDispose` don't exist before Node.js 18.x / Chrome 120. Add the two-line polyfill at your entry point. `DisposableStack` and `AsyncDisposableStack` need a more substantial polyfill (e.g. from `@microsoft/using-polyfill`).
|
||||
|
||||
**`using` disposes in LIFO order.** Resources declared later in a scope are disposed first. Declare in the order you want reversed cleanup (acquisition order). `DisposableStack.defer` also runs in LIFO order.
|
||||
|
||||
**Inferred type predicates have if-and-only-if semantics.** `x => !!x` does NOT infer `x is NonNullable<T>` because `0`, `""`, and `false` are falsy but not absent. TypeScript correctly refuses the predicate. Use `x => x !== undefined` or `x => x !== null` for precise null/undefined filters. If a predicate isn't being inferred, the false branch is probably ambiguous.
|
||||
|
||||
**`--verbatimModuleSyntax` breaks CJS `require` emit.** Under this flag ESM `import`/`export` is emitted verbatim. You cannot produce `require()` calls from standard `import` syntax. For CJS output you must use `import foo = require("foo")` and `export = { ... }` syntax explicitly.
|
||||
|
||||
**`NoInfer<T>` doesn't prevent `T` from being resolved, only from being contributed at that position.** Other parameters can still infer `T`. It means "don't use me as an inference candidate", not "block `T` from being resolved".
|
||||
|
||||
**`--isolatedDeclarations` requires explicit return types on all exports.** Exported arrow functions, function declarations, and class methods all need annotations if their return type isn't trivially inferrable from a literal or type assertion. Editor quick-fixes can add them automatically.
|
||||
|
||||
**Standard decorators are incompatible with `--experimentalDecorators`.** Different type signatures, metadata model, and emit. A decorator written for one will not work with the other. `--emitDecoratorMetadata` is not supported with standard decorators. Don't mix the two systems in one project.
|
||||
|
||||
**`import defer` does not downlevel.** TypeScript does not transform `import defer` to polyfill-compatible code. The module is still _loaded_ eagerly (must exist); only _evaluation_ is deferred. Only use it under `--module preserve` or `esnext` with a runtime or bundler that supports it.
|
||||
|
||||
**`--erasableSyntaxOnly` prohibits parameter properties.** `constructor(public x: number)` is not allowed. Expand to an explicit field declaration plus assignment in the constructor body.
|
||||
|
||||
**Closure narrowing is invalidated if the variable is assigned anywhere in a nested function.** TypeScript cannot know when a nested function will run, so any assignment to a `let`/param inside a nested function — even a no-op like `value = value` — invalidates narrowing for all closures in the outer scope. Only the outer "no further assignments after this point" pattern is safe.
|
||||
|
||||
**Constant indexed access narrowing requires both `obj` and `key` to be unmodified between the check and the use.** If either is a `let` that could be reassigned, TypeScript will not narrow `obj[key]`. Extract the value to a `const` in that case.
|
||||
|
||||
**`switch (true)` narrowing does not carry across fall-through cases.** In a `switch (true)`, each `case` condition narrows independently. A variable narrowed in `case typeof x === "string":` that falls through to the next case will have its narrowing widened by the next condition, not accumulated from the previous one.
|
||||
|
||||
**`const` type parameter modifier falls back when constraint is mutable.** `<const T extends string[]>(args: T)` falls back to `string[]` because `readonly ["a", "b"]` isn't assignable to `string[]`. Use `<const T extends readonly string[]>` for arrays.
|
||||
|
||||
**`assert` import syntax errors under `--module nodenext` since 5.8.** Any remaining `import x from "..." assert { ... }` must be updated to `import x from "..." with { ... }`.
|
||||
|
||||
**`Array.prototype.filter(x => x !== null)` now narrows to non-null (5.5).** This is almost always correct, but if you intentionally needed the nullable type downstream, add an explicit annotation: `const items: (T | null)[] = arr.filter(x => x !== null)`.
|
||||
|
||||
## Behavioral changes that affect code
|
||||
|
||||
- **All enums are union enums** (5.0): Every enum member gets its own literal type. Out-of-domain literal assignment to an enum type now errors. Cross-enum assignment between enums with identical names but differing values now errors.
|
||||
- **Relational operators no longer allow implicit string/number coercions** (5.0): `ns > 4` where `ns: number | string` is a type error. Use `+ns > 4` to explicitly coerce.
|
||||
- **`--module`/`--moduleResolution` must agree on node flavor** (5.2): Mixing `--module nodenext` with `--moduleResolution bundler` is an error. Use `--module nodenext` alone or `--module esnext --moduleResolution bundler`.
|
||||
- **Deprecations from 5.0 become hard errors in 5.5**: `--importsNotUsedAsValues`, `--preserveValueImports`, `--target ES3`, `--out`, and several others are fully removed in 5.5. They can no longer be specified, even with `"ignoreDeprecations": "5.0"`. Migrate to `--verbatimModuleSyntax` for the import flags.
|
||||
- **Type-only imports conflicting with local values** (5.4): Under `--isolatedModules`, `import { Foo } from "..."` where a local `let Foo` also exists now errors. Use `import type { Foo }` or `import { type Foo }`.
|
||||
- **Reference directives no longer synthesized or preserved in declaration emit** (5.5): `/// <reference types="node" />` TypeScript used to add automatically is no longer emitted. User-written directives are dropped unless they carry `preserve="true"`. Update library `tsconfig.json` if you relied on this.
|
||||
- **`.mts` files never emit CJS; `.cts` files never emit ESM** (5.6): Regardless of `--module` setting. Previously the extension was ignored in some modes.
|
||||
- **JSON imports under `--module nodenext` require `with { type: "json" }`** (5.7): `import data from "./config.json"` without the attribute is now a type error.
|
||||
- **`TypedArray`s are now generic** (5.7): `Uint8Array` is `Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>`. Code passing `Buffer` (from `@types/node`) to typed-array parameters may see new errors. Update `@types/node` to a version that matches.
|
||||
- **`import assert { ... }` is an error under `--module nodenext`** (5.8): Node.js 22 dropped support for the old syntax. Use `with { ... }`.
|
||||
- **`types` defaults to `[]` in 6.0**: All implicit `@types/*` loading stops. Add an explicit `"types": ["node"]` or the array will remain empty. Using `"types": ["*"]` restores the 5.x behavior.
|
||||
- **`rootDir` defaults to `.` (the tsconfig directory) in 6.0**: Previously inferred from the common ancestor of all source files. Projects with `"include": ["./src"]` and no explicit `rootDir` will now emit into `dist/src/` instead of `dist/`. Add `"rootDir": "./src"` to fix.
|
||||
- **`strict` defaults to `true` in 6.0**: Projects that were implicitly not strict will see new errors. Set `"strict": false` explicitly if you're not ready to fix them.
|
||||
- **`--baseUrl` deprecated in 6.0** and no longer acts as a module resolution root. Add explicit prefixes to your `paths` entries instead.
|
||||
- **`--moduleResolution node` (node10) deprecated in 6.0**: Removed in 7.0. Migrate to `nodenext` or `bundler`.
|
||||
- **`amd`, `umd`, `systemjs`, `none` module targets deprecated in 6.0**: Removed in 7.0. Migrate to a bundler.
|
||||
- **`--outFile` removed in 6.0**: Use a bundler (esbuild, Rollup, Webpack, etc.).
|
||||
- **`module Foo { }` syntax removed in 6.0**: Rename all such declarations to `namespace Foo { }`.
|
||||
- **`--esModuleInterop false` and `--allowSyntheticDefaultImports false` removed in 6.0**: Safe interop is now always on. Default imports from CJS modules (`import express from "express"`) are always valid.
|
||||
- **Explicit `typeRoots` disables upward `node_modules/@types` fallback** (5.1): When `typeRoots` is specified and a lookup fails in those directories, TypeScript no longer walks parent directories for `@types`. If you relied on the fallback, add `"./node_modules/@types"` explicitly to your `typeRoots` array.
|
||||
- **`super.` on instance field properties is a type error** (5.3): Calling `super.foo()` where `foo` is a class field (arrow function assigned in the constructor) rather than a prototype method now errors. Instance fields don't exist on the prototype; `super.field` is `undefined` at runtime.
|
||||
- **`--build` always emits `.tsbuildinfo`** (5.6): Previously only written when `--incremental` or `--composite` was set. Now written unconditionally in any `--build` invocation. Update `.gitignore` or CI artifact management if needed.
|
||||
- **`.mts`/`.cts` extensions and `package.json` `"type"` respected in all module modes** (5.6): Format-specific extensions and the `"type"` field inside `node_modules` are now honored regardless of `--module` setting (except `amd`, `umd`, `system`). A `.mts` file will never emit CJS output even under `--module commonjs`.
|
||||
- **Granular return expression checking** (5.8): Each branch of a conditional expression (`cond ? a : b`) directly inside a `return` statement is now checked individually against the declared return type. Previously an `any`-typed branch could silently suppress type errors in the other branch.
|
||||
@@ -4,7 +4,7 @@ description: |
|
||||
inputs:
|
||||
version:
|
||||
description: "The Go version to use."
|
||||
default: "1.25.7"
|
||||
default: "1.25.8"
|
||||
use-cache:
|
||||
description: "Whether to use the cache."
|
||||
default: "true"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
enabled: true
|
||||
preservePullRequestTitle: true
|
||||
@@ -82,9 +82,6 @@ updates:
|
||||
mui:
|
||||
patterns:
|
||||
- "@mui*"
|
||||
radix:
|
||||
patterns:
|
||||
- "@radix-ui/*"
|
||||
react:
|
||||
patterns:
|
||||
- "react"
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# Automatically backport merged PRs to the last N release branches when the
|
||||
# "backport" label is applied. Works whether the label is added before or
|
||||
# after the PR is merged.
|
||||
#
|
||||
# Usage:
|
||||
# 1. Add the "backport" label to a PR targeting main.
|
||||
# 2. When the PR merges (or if already merged), the workflow detects the
|
||||
# latest release/* branches and opens one cherry-pick PR per branch.
|
||||
#
|
||||
# The created backport PRs follow existing repo conventions:
|
||||
# - Branch: backport/<pr>-to-<version>
|
||||
# - Title: <original PR title> (#<pr>)
|
||||
# - Body: links back to the original PR and merge commit
|
||||
|
||||
name: Backport
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
# Prevent duplicate runs for the same PR when both 'closed' and 'labeled'
|
||||
# fire in quick succession.
|
||||
concurrency:
|
||||
group: backport-${{ github.event.pull_request.number }}
|
||||
|
||||
jobs:
|
||||
detect:
|
||||
name: Detect target branches
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'backport')
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
branches: ${{ steps.find.outputs.branches }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Need all refs to discover release branches.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Find latest release branches
|
||||
id: find
|
||||
run: |
|
||||
# List remote release branches matching the exact release/2.X
|
||||
# pattern (no suffixes like release/2.31_hotfix), sort by minor
|
||||
# version descending, and take the top 3.
|
||||
BRANCHES=$(
|
||||
git branch -r \
|
||||
| grep -E '^\s*origin/release/2\.[0-9]+$' \
|
||||
| sed 's|.*origin/||' \
|
||||
| sort -t. -k2 -n -r \
|
||||
| head -3
|
||||
)
|
||||
|
||||
if [ -z "$BRANCHES" ]; then
|
||||
echo "No release branches found."
|
||||
echo "branches=[]" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Convert to JSON array for the matrix.
|
||||
JSON=$(echo "$BRANCHES" | jq -Rnc '[inputs | select(length > 0)]')
|
||||
echo "branches=$JSON" >> "$GITHUB_OUTPUT"
|
||||
echo "Will backport to: $JSON"
|
||||
|
||||
backport:
|
||||
name: "Backport to ${{ matrix.branch }}"
|
||||
needs: detect
|
||||
if: needs.detect.outputs.branches != '[]'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
branch: ${{ fromJson(needs.detect.outputs.branches) }}
|
||||
fail-fast: false
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Full history required for cherry-pick.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cherry-pick and open PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
RELEASE_VERSION="${{ matrix.branch }}"
|
||||
# Strip the release/ prefix for naming.
|
||||
VERSION="${RELEASE_VERSION#release/}"
|
||||
BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${VERSION}"
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Check if backport branch already exists (idempotency for re-runs).
|
||||
if git ls-remote --exit-code origin "refs/heads/${BACKPORT_BRANCH}" >/dev/null 2>&1; then
|
||||
echo "Backport branch ${BACKPORT_BRANCH} already exists, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create the backport branch from the target release branch.
|
||||
git checkout -b "$BACKPORT_BRANCH" "origin/${RELEASE_VERSION}"
|
||||
|
||||
# Cherry-pick the merge commit. Use -x to record provenance and
|
||||
# -m1 to pick the first parent (the main branch side).
|
||||
CONFLICTS=false
|
||||
if ! git cherry-pick -x -m1 "$MERGE_SHA"; then
|
||||
echo "::warning::Cherry-pick to ${RELEASE_VERSION} had conflicts."
|
||||
CONFLICTS=true
|
||||
|
||||
# Abort the failed cherry-pick and create an empty commit
|
||||
# explaining the situation.
|
||||
git cherry-pick --abort
|
||||
git commit --allow-empty -m "Cherry-pick of #${PR_NUMBER} requires manual resolution
|
||||
|
||||
The automatic cherry-pick of ${MERGE_SHA} to ${RELEASE_VERSION} had conflicts.
|
||||
Please cherry-pick manually:
|
||||
|
||||
git cherry-pick -x -m1 ${MERGE_SHA}"
|
||||
fi
|
||||
|
||||
git push origin "$BACKPORT_BRANCH"
|
||||
|
||||
TITLE="${PR_TITLE} (#${PR_NUMBER})"
|
||||
BODY=$(cat <<EOF
|
||||
Backport of ${PR_URL}
|
||||
|
||||
Original PR: #${PR_NUMBER} — ${PR_TITLE}
|
||||
Merge commit: ${MERGE_SHA}
|
||||
EOF
|
||||
)
|
||||
|
||||
if [ "$CONFLICTS" = true ]; then
|
||||
TITLE="${TITLE} (conflicts)"
|
||||
BODY="${BODY}
|
||||
|
||||
> [!WARNING]
|
||||
> The automatic cherry-pick had conflicts.
|
||||
> Please resolve manually by cherry-picking the original merge commit:
|
||||
>
|
||||
> \`\`\`
|
||||
> git fetch origin ${BACKPORT_BRANCH}
|
||||
> git checkout ${BACKPORT_BRANCH}
|
||||
> git reset --hard origin/${RELEASE_VERSION}
|
||||
> git cherry-pick -x -m1 ${MERGE_SHA}
|
||||
> # resolve conflicts, then push
|
||||
> \`\`\`"
|
||||
fi
|
||||
|
||||
# Check if a PR already exists for this branch (idempotency
|
||||
# for re-runs).
|
||||
EXISTING_PR=$(gh pr list --head "$BACKPORT_BRANCH" --base "$RELEASE_VERSION" --state all --json number --jq '.[0].number // empty')
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "PR #${EXISTING_PR} already exists for ${BACKPORT_BRANCH}, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
gh pr create \
|
||||
--base "$RELEASE_VERSION" \
|
||||
--head "$BACKPORT_BRANCH" \
|
||||
--title "$TITLE" \
|
||||
--body "$BODY"
|
||||
@@ -0,0 +1,139 @@
|
||||
# Automatically cherry-pick merged PRs to the latest release branch when the
|
||||
# "cherry-pick" label is applied. Works whether the label is added before or
|
||||
# after the PR is merged.
|
||||
#
|
||||
# Usage:
|
||||
# 1. Add the "cherry-pick" label to a PR targeting main.
|
||||
# 2. When the PR merges (or if already merged), the workflow detects the
|
||||
# latest release/* branch and opens a cherry-pick PR against it.
|
||||
#
|
||||
# The created PRs follow existing repo conventions:
|
||||
# - Branch: backport/<pr>-to-<version>
|
||||
# - Title: <original PR title> (#<pr>)
|
||||
# - Body: links back to the original PR and merge commit
|
||||
|
||||
name: Cherry-pick to release
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
# Prevent duplicate runs for the same PR when both 'closed' and 'labeled'
|
||||
# fire in quick succession.
|
||||
concurrency:
|
||||
group: cherry-pick-${{ github.event.pull_request.number }}
|
||||
|
||||
jobs:
|
||||
cherry-pick:
|
||||
name: Cherry-pick to latest release
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'cherry-pick')
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Full history required for cherry-pick and branch discovery.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cherry-pick and open PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Find the latest release branch matching the exact release/2.X
|
||||
# pattern (no suffixes like release/2.31_hotfix).
|
||||
RELEASE_BRANCH=$(
|
||||
git branch -r \
|
||||
| grep -E '^\s*origin/release/2\.[0-9]+$' \
|
||||
| sed 's|.*origin/||' \
|
||||
| sort -t. -k2 -n -r \
|
||||
| head -1
|
||||
)
|
||||
|
||||
if [ -z "$RELEASE_BRANCH" ]; then
|
||||
echo "::error::No release branch found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Strip the release/ prefix for naming.
|
||||
VERSION="${RELEASE_BRANCH#release/}"
|
||||
BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${VERSION}"
|
||||
|
||||
echo "Target branch: $RELEASE_BRANCH"
|
||||
echo "Backport branch: $BACKPORT_BRANCH"
|
||||
|
||||
# Check if backport branch already exists (idempotency for re-runs).
|
||||
if git ls-remote --exit-code origin "refs/heads/${BACKPORT_BRANCH}" >/dev/null 2>&1; then
|
||||
echo "Branch ${BACKPORT_BRANCH} already exists, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Create the backport branch from the target release branch.
|
||||
git checkout -b "$BACKPORT_BRANCH" "origin/${RELEASE_BRANCH}"
|
||||
|
||||
# Cherry-pick the merge commit. Use -x to record provenance and
|
||||
# -m1 to pick the first parent (the main branch side).
|
||||
CONFLICT=false
|
||||
if ! git cherry-pick -x -m1 "$MERGE_SHA"; then
|
||||
CONFLICT=true
|
||||
echo "::warning::Cherry-pick to ${RELEASE_BRANCH} had conflicts."
|
||||
|
||||
# Abort the failed cherry-pick and create an empty commit with
|
||||
# instructions so the PR can still be opened.
|
||||
git cherry-pick --abort
|
||||
git commit --allow-empty -m "cherry-pick of #${PR_NUMBER} failed — resolve conflicts manually
|
||||
|
||||
Cherry-pick of ${MERGE_SHA} onto ${RELEASE_BRANCH} had conflicts.
|
||||
To resolve:
|
||||
git fetch origin ${BACKPORT_BRANCH}
|
||||
git checkout ${BACKPORT_BRANCH}
|
||||
git cherry-pick -x -m1 ${MERGE_SHA}
|
||||
# resolve conflicts
|
||||
git push origin ${BACKPORT_BRANCH}"
|
||||
fi
|
||||
|
||||
git push origin "$BACKPORT_BRANCH"
|
||||
|
||||
BODY=$(cat <<EOF
|
||||
Cherry-pick of ${PR_URL}
|
||||
|
||||
Original PR: #${PR_NUMBER} — ${PR_TITLE}
|
||||
Merge commit: ${MERGE_SHA}
|
||||
EOF
|
||||
)
|
||||
|
||||
TITLE="${PR_TITLE} (#${PR_NUMBER})"
|
||||
if [ "$CONFLICT" = true ]; then
|
||||
TITLE="[CONFLICT] ${TITLE}"
|
||||
fi
|
||||
|
||||
# Check if a PR already exists for this branch (idempotency
|
||||
# for re-runs). Use --state all to catch closed/merged PRs too.
|
||||
EXISTING_PR=$(gh pr list --head "$BACKPORT_BRANCH" --base "$RELEASE_BRANCH" --state all --json number --jq '.[0].number // empty')
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "PR #${EXISTING_PR} already exists for ${BACKPORT_BRANCH}, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
gh pr create \
|
||||
--base "$RELEASE_BRANCH" \
|
||||
--head "$BACKPORT_BRANCH" \
|
||||
--title "$TITLE" \
|
||||
--body "$BODY"
|
||||
+21
-20
@@ -35,7 +35,7 @@ jobs:
|
||||
tailnet-integration: ${{ steps.filter.outputs.tailnet-integration }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -77,6 +77,7 @@ jobs:
|
||||
# Main repo directories for completeness in case other files are
|
||||
# touched:
|
||||
- "agent/**"
|
||||
- "aibridge/**"
|
||||
- "cli/**"
|
||||
- "cmd/**"
|
||||
- "coderd/**"
|
||||
@@ -157,7 +158,7 @@ jobs:
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -204,7 +205,7 @@ jobs:
|
||||
|
||||
# Needed for helm chart linting
|
||||
- name: Install helm
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
|
||||
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
|
||||
with:
|
||||
version: v3.9.2
|
||||
continue-on-error: true
|
||||
@@ -247,7 +248,7 @@ jobs:
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -272,7 +273,7 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -327,7 +328,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -379,7 +380,7 @@ jobs:
|
||||
- windows-2022
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -575,7 +576,7 @@ jobs:
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -637,7 +638,7 @@ jobs:
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -709,7 +710,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -736,7 +737,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -769,7 +770,7 @@ jobs:
|
||||
name: ${{ matrix.variant.name }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -849,7 +850,7 @@ jobs:
|
||||
if: needs.changes.outputs.site == 'true' || needs.changes.outputs.ci == 'true'
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -870,7 +871,7 @@ jobs:
|
||||
# the check to pass. This is desired in PRs, but not in mainline.
|
||||
- name: Publish to Chromatic (non-mainline)
|
||||
if: github.ref != 'refs/heads/main' && github.repository_owner == 'coder'
|
||||
uses: chromaui/action@07791f8243f4cb2698bf4d00426baf4b2d1cb7e0 # v13.3.5
|
||||
uses: chromaui/action@f191a0224b10e1a38b2091cefb7b7a2337009116 # v16.0.0
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
STORYBOOK: true
|
||||
@@ -902,7 +903,7 @@ jobs:
|
||||
# infinitely "in progress" in mainline unless we re-review each build.
|
||||
- name: Publish to Chromatic (mainline)
|
||||
if: github.ref == 'refs/heads/main' && github.repository_owner == 'coder'
|
||||
uses: chromaui/action@07791f8243f4cb2698bf4d00426baf4b2d1cb7e0 # v13.3.5
|
||||
uses: chromaui/action@f191a0224b10e1a38b2091cefb7b7a2337009116 # v16.0.0
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
STORYBOOK: true
|
||||
@@ -930,7 +931,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -1005,7 +1006,7 @@ jobs:
|
||||
if: always()
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -1043,7 +1044,7 @@ jobs:
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -1097,7 +1098,7 @@ jobs:
|
||||
IMAGE: ghcr.io/coder/coder-preview:${{ steps.build-docker.outputs.tag }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -1479,7 +1480,7 @@ jobs:
|
||||
if: needs.changes.outputs.db == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0
|
||||
uses: dependabot/fetch-metadata@ffa630c65fa7e0ecfa0625b5ceda64399aea1b36 # v3.0.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
verdict: ${{ steps.check.outputs.verdict }} # DEPLOY or NOOP
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
packages: write # to retag image as dogfood
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
needs: deploy
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -240,6 +240,7 @@ jobs:
|
||||
- name: Create Coder Task for Documentation Check
|
||||
if: steps.check-secrets.outputs.skip != 'true'
|
||||
id: create_task
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/create-task-action
|
||||
with:
|
||||
coder-url: ${{ secrets.DOC_CHECK_CODER_URL }}
|
||||
@@ -254,8 +255,21 @@ jobs:
|
||||
github-issue-url: ${{ steps.determine-context.outputs.pr_url }}
|
||||
comment-on-issue: false
|
||||
|
||||
- name: Handle Task Creation Failure
|
||||
if: steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome != 'success'
|
||||
run: |
|
||||
{
|
||||
echo "## Documentation Check Task"
|
||||
echo ""
|
||||
echo "⚠️ The external Coder task service was unavailable, so this"
|
||||
echo "advisory documentation check did not run."
|
||||
echo ""
|
||||
echo "Maintainers can rerun the workflow or trigger it manually"
|
||||
echo "after the service recovers."
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Write Task Info
|
||||
if: steps.check-secrets.outputs.skip != 'true'
|
||||
if: steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success'
|
||||
env:
|
||||
TASK_CREATED: ${{ steps.create_task.outputs.task-created }}
|
||||
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
|
||||
@@ -273,7 +287,7 @@ jobs:
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Wait for Task Completion
|
||||
if: steps.check-secrets.outputs.skip != 'true'
|
||||
if: steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success'
|
||||
id: wait_task
|
||||
env:
|
||||
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
|
||||
@@ -363,7 +377,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Fetch Task Logs
|
||||
if: always() && steps.check-secrets.outputs.skip != 'true'
|
||||
if: always() && steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success'
|
||||
env:
|
||||
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
|
||||
run: |
|
||||
@@ -376,7 +390,7 @@ jobs:
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Cleanup Task
|
||||
if: always() && steps.check-secrets.outputs.skip != 'true'
|
||||
if: always() && steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success'
|
||||
env:
|
||||
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
|
||||
run: |
|
||||
@@ -390,6 +404,7 @@ jobs:
|
||||
- name: Write Final Summary
|
||||
if: always() && steps.check-secrets.outputs.skip != 'true'
|
||||
env:
|
||||
CREATE_TASK_OUTCOME: ${{ steps.create_task.outcome }}
|
||||
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
|
||||
TASK_MESSAGE: ${{ steps.wait_task.outputs.task_message }}
|
||||
RESULT_URI: ${{ steps.wait_task.outputs.result_uri }}
|
||||
@@ -400,10 +415,15 @@ jobs:
|
||||
echo "---"
|
||||
echo "### Result"
|
||||
echo ""
|
||||
echo "**Status:** ${TASK_MESSAGE:-Task completed}"
|
||||
if [[ -n "${RESULT_URI}" ]]; then
|
||||
echo "**Comment:** ${RESULT_URI}"
|
||||
if [[ "${CREATE_TASK_OUTCOME}" == "success" ]]; then
|
||||
echo "**Status:** ${TASK_MESSAGE:-Task completed}"
|
||||
if [[ -n "${RESULT_URI}" ]]; then
|
||||
echo "**Comment:** ${RESULT_URI}"
|
||||
fi
|
||||
echo ""
|
||||
echo "Task \`${TASK_NAME}\` has been cleaned up."
|
||||
else
|
||||
echo "**Status:** Skipped because the external Coder task"
|
||||
echo "service was unavailable."
|
||||
fi
|
||||
echo ""
|
||||
echo "Task \`${TASK_NAME}\` has been cleaned up."
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
if: github.repository_owner == 'coder'
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-4' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- "release/2.[0-9]+"
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -29,9 +27,9 @@ jobs:
|
||||
|
||||
- name: Detect next release version
|
||||
id: version
|
||||
# Find the highest release/2.X branch (exact pattern, no suffixes like
|
||||
# release/2.31_hotfix) and derive the next minor version for the release
|
||||
# currently in development on main.
|
||||
# Find the highest release/2.X branch (exact pattern, no suffixes
|
||||
# like release/2.31_hotfix) and derive the next minor version for
|
||||
# the release currently in development on main.
|
||||
run: |
|
||||
LATEST_MINOR=$(git branch -r | grep -E '^\s*origin/release/2\.[0-9]+$' | \
|
||||
sed 's/.*release\/2\.//' | sort -n | tail -1)
|
||||
@@ -40,8 +38,10 @@ jobs:
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "version=2.$((LATEST_MINOR + 1))" >> "$GITHUB_OUTPUT"
|
||||
NEXT="2.$((LATEST_MINOR + 1))"
|
||||
echo "version=$NEXT" >> "$GITHUB_OUTPUT"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Detected next release: $NEXT"
|
||||
|
||||
- name: Sync issues
|
||||
id: sync
|
||||
@@ -51,6 +51,7 @@ jobs:
|
||||
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
|
||||
command: sync
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
timeout: 300
|
||||
|
||||
sync-release-branch:
|
||||
@@ -76,6 +77,7 @@ jobs:
|
||||
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
|
||||
command: sync
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
timeout: 300
|
||||
|
||||
code-freeze:
|
||||
@@ -106,38 +108,3 @@ jobs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
timeout: 300
|
||||
|
||||
complete:
|
||||
name: Complete Linear release
|
||||
if: github.event_name == 'release'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Extract release version
|
||||
id: version
|
||||
# Strip "v" prefix and patch: "v2.31.0" -> "2.31". Also detect whether
|
||||
# this is a minor release (v*.*.0) — patch releases (v2.31.1, v2.31.2,
|
||||
# ...) are grouped into the same Linear release and must not re-complete
|
||||
# it after it has already shipped.
|
||||
run: |
|
||||
VERSION=$(echo "$TAG" | sed 's/^v//' | cut -d. -f1,2)
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.0$ ]]; then
|
||||
echo "is_minor=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_minor=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
env:
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Complete release
|
||||
id: complete
|
||||
if: steps.version.outputs.is_minor == 'true'
|
||||
uses: linear/linear-release-action@755d50b5adb7dd42b976ee9334952745d62ceb2d # v0.6.0
|
||||
with:
|
||||
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
|
||||
command: complete
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
timeout: 300
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- windows-2022
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
PR_OPEN: ${{ steps.check_pr.outputs.pr_open }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
pull-requests: write # needed for commenting on PRs
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -228,7 +228,7 @@ jobs:
|
||||
CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -288,7 +288,7 @@ jobs:
|
||||
PR_HOSTNAME: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ on:
|
||||
options:
|
||||
- mainline
|
||||
- stable
|
||||
- rc
|
||||
release_notes:
|
||||
description: Release notes for the publishing the release. This is required to create a release.
|
||||
dry_run:
|
||||
@@ -80,7 +81,7 @@ jobs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -119,13 +120,23 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2.10.2 -> release/2.10
|
||||
# Derive the release branch from the version tag.
|
||||
# Non-RC releases must be on a release/X.Y branch.
|
||||
# RC tags are allowed on any branch (typically main).
|
||||
version="$(./scripts/version.sh)"
|
||||
release_branch=release/${version%.*}
|
||||
branch_contains_tag=$(git branch --remotes --contains "${GITHUB_REF}" --list "*/${release_branch}" --format='%(refname)')
|
||||
if [[ -z "${branch_contains_tag}" ]]; then
|
||||
echo "Ref tag must exist in a branch named ${release_branch} when creating a release, did you use scripts/release.sh?"
|
||||
exit 1
|
||||
# Strip any pre-release suffix first (e.g. 2.32.0-rc.0 -> 2.32.0)
|
||||
base_version="${version%%-*}"
|
||||
# Then strip patch to get major.minor (e.g. 2.32.0 -> 2.32)
|
||||
release_branch="release/${base_version%.*}"
|
||||
|
||||
if [[ "$version" == *-rc.* ]]; then
|
||||
echo "RC release detected — skipping release branch check (RC tags are cut from main)."
|
||||
else
|
||||
branch_contains_tag=$(git branch --remotes --contains "${GITHUB_REF}" --list "*/${release_branch}" --format='%(refname)')
|
||||
if [[ -z "${branch_contains_tag}" ]]; then
|
||||
echo "Ref tag must exist in a branch named ${release_branch} when creating a non-RC release, did you use scripts/release.sh?"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${CODER_RELEASE_NOTES}" ]]; then
|
||||
@@ -531,6 +542,9 @@ jobs:
|
||||
if [[ $CODER_RELEASE_CHANNEL == "stable" ]]; then
|
||||
publish_args+=(--stable)
|
||||
fi
|
||||
if [[ $CODER_RELEASE_CHANNEL == "rc" ]]; then
|
||||
publish_args+=(--rc)
|
||||
fi
|
||||
if [[ $CODER_DRY_RUN == *t* ]]; then
|
||||
publish_args+=(--dry-run)
|
||||
fi
|
||||
@@ -563,6 +577,35 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
CREATED_LATEST_TAG: ${{ steps.build_docker.outputs.created_latest_tag }}
|
||||
|
||||
# Mark the Linear release as shipped.
|
||||
- name: Extract Linear release version
|
||||
if: ${{ !inputs.dry_run }}
|
||||
id: linear_version
|
||||
run: |
|
||||
# Skip RC releases — they must not complete the Linear release.
|
||||
if [[ "$VERSION" == *-rc* ]]; then
|
||||
echo "RC release (${VERSION}), skipping Linear release completion."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
# Strip patch to get the Linear release version (e.g. 2.32.0 -> 2.32).
|
||||
linear_version=$(echo "$VERSION" | cut -d. -f1,2)
|
||||
echo "version=$linear_version" >> "$GITHUB_OUTPUT"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Completing Linear release ${linear_version}"
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
|
||||
- name: Complete Linear release
|
||||
if: ${{ !inputs.dry_run && steps.linear_version.outputs.skip != 'true' }}
|
||||
continue-on-error: true
|
||||
uses: linear/linear-release-action@755d50b5adb7dd42b976ee9334952745d62ceb2d # v0.6.0
|
||||
with:
|
||||
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
|
||||
command: complete
|
||||
version: ${{ steps.linear_version.outputs.version }}
|
||||
timeout: 300
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
|
||||
with:
|
||||
@@ -614,7 +657,7 @@ jobs:
|
||||
retention-days: 7
|
||||
|
||||
- name: Send repository-dispatch event
|
||||
if: ${{ !inputs.dry_run }}
|
||||
if: ${{ !inputs.dry_run && inputs.release_channel != 'rc' }}
|
||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
|
||||
with:
|
||||
token: ${{ secrets.CDRCI_GITHUB_TOKEN }}
|
||||
@@ -630,7 +673,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -702,11 +745,11 @@ jobs:
|
||||
name: Publish to winget-pkgs
|
||||
runs-on: windows-latest
|
||||
needs: release
|
||||
if: ${{ !inputs.dry_run }}
|
||||
if: ${{ !inputs.dry_run && inputs.release_channel != 'rc' }}
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -47,6 +47,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v3.29.5
|
||||
with:
|
||||
languages: go, javascript
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
rm Makefile
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v3.29.5
|
||||
|
||||
- name: Send Slack notification on failure
|
||||
if: ${{ failure() }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
actions: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ typ = "typ"
|
||||
styl = "styl"
|
||||
edn = "edn"
|
||||
Inferrable = "Inferrable"
|
||||
IIF = "IIF"
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
pull-requests: write # required to post PR review comments by the action
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -46,6 +46,14 @@ jobs:
|
||||
echo " replacement: \"https://github.com/coder/coder/tree/${HEAD_SHA}/\""
|
||||
} >> .github/.linkspector.yml
|
||||
|
||||
# TODO: Remove this workaround once action-linkspector sets
|
||||
# package-manager-cache: false in its internal setup-node step.
|
||||
# See: https://github.com/UmbrellaDocs/action-linkspector/issues/54
|
||||
- name: Enable corepack and create pnpm store
|
||||
run: |
|
||||
corepack enable pnpm
|
||||
mkdir -p "$(pnpm store path --silent)"
|
||||
|
||||
- name: Check Markdown links
|
||||
uses: umbrelladocs/action-linkspector@37c85bcde51b30bf929936502bac6bfb7e8f0a4d # v1.4.1
|
||||
id: markdown-link-check
|
||||
|
||||
@@ -103,3 +103,6 @@ PLAN.md
|
||||
|
||||
# Ignore any dev licenses
|
||||
license.txt
|
||||
-e
|
||||
# Agent planning documents (local working files).
|
||||
docs/plans/
|
||||
|
||||
@@ -6,6 +6,21 @@ linters-settings:
|
||||
# goal: 100
|
||||
threshold: 412
|
||||
|
||||
depguard:
|
||||
rules:
|
||||
aibridge_boundary:
|
||||
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
|
||||
|
||||
@@ -110,6 +110,9 @@ app, err := api.Database.GetOAuth2ProviderAppByClientID(ctx, clientID)
|
||||
- For experimental or unstable API paths, skip public doc generation with
|
||||
`// @x-apidocgen {"skip": true}` after the `@Router` annotation. This
|
||||
keeps them out of the published API reference until they stabilize.
|
||||
- Experimental chat endpoints in `coderd/exp_chats.go` omit swagger
|
||||
annotations entirely. Do not add `@Summary`, `@Router`, or other
|
||||
swagger comments to handlers in that file.
|
||||
|
||||
### Database Query Naming
|
||||
|
||||
|
||||
@@ -988,6 +988,7 @@ coderd/httpmw/loggermw/loggermock/loggermock.go: coderd/httpmw/loggermw/logger.g
|
||||
|
||||
codersdk/workspacesdk/agentconnmock/agentconnmock.go: codersdk/workspacesdk/agentconn.go
|
||||
go generate ./codersdk/workspacesdk/agentconnmock/
|
||||
./scripts/format_go_file.sh "$@"
|
||||
touch "$@"
|
||||
|
||||
$(AIBRIDGED_MOCKS): enterprise/aibridged/client.go enterprise/aibridged/pool.go
|
||||
|
||||
+30
-14
@@ -38,6 +38,7 @@ import (
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/clistat"
|
||||
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||
"github.com/coder/coder/v2/agent/agentcontextconfig"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/agent/agentfiles"
|
||||
"github.com/coder/coder/v2/agent/agentgit"
|
||||
@@ -101,6 +102,8 @@ type Options struct {
|
||||
ReportMetadataInterval time.Duration
|
||||
ServiceBannerRefreshInterval time.Duration
|
||||
BlockFileTransfer bool
|
||||
BlockReversePortForwarding bool
|
||||
BlockLocalPortForwarding bool
|
||||
Execer agentexec.Execer
|
||||
Devcontainers bool
|
||||
DevcontainerAPIOptions []agentcontainers.Option // Enable Devcontainers for these to be effective.
|
||||
@@ -213,6 +216,8 @@ func New(options Options) Agent {
|
||||
subsystems: options.Subsystems,
|
||||
logSender: agentsdk.NewLogSender(options.Logger),
|
||||
blockFileTransfer: options.BlockFileTransfer,
|
||||
blockReversePortForwarding: options.BlockReversePortForwarding,
|
||||
blockLocalPortForwarding: options.BlockLocalPortForwarding,
|
||||
|
||||
prometheusRegistry: prometheusRegistry,
|
||||
metrics: newAgentMetrics(prometheusRegistry),
|
||||
@@ -279,6 +284,8 @@ type agent struct {
|
||||
sshServer *agentssh.Server
|
||||
sshMaxTimeout time.Duration
|
||||
blockFileTransfer bool
|
||||
blockReversePortForwarding bool
|
||||
blockLocalPortForwarding bool
|
||||
|
||||
lifecycleUpdate chan struct{}
|
||||
lifecycleReported chan codersdk.WorkspaceAgentLifecycle
|
||||
@@ -308,12 +315,13 @@ type agent struct {
|
||||
containerAPI *agentcontainers.API
|
||||
gitAPIOptions []agentgit.Option
|
||||
|
||||
filesAPI *agentfiles.API
|
||||
gitAPI *agentgit.API
|
||||
processAPI *agentproc.API
|
||||
desktopAPI *agentdesktop.API
|
||||
mcpManager *agentmcp.Manager
|
||||
mcpAPI *agentmcp.API
|
||||
filesAPI *agentfiles.API
|
||||
gitAPI *agentgit.API
|
||||
processAPI *agentproc.API
|
||||
desktopAPI *agentdesktop.API
|
||||
mcpManager *agentmcp.Manager
|
||||
mcpAPI *agentmcp.API
|
||||
contextConfigAPI *agentcontextconfig.API
|
||||
|
||||
socketServerEnabled bool
|
||||
socketPath string
|
||||
@@ -329,12 +337,14 @@ func (a *agent) TailnetConn() *tailnet.Conn {
|
||||
func (a *agent) init() {
|
||||
// pass the "hard" context because we explicitly close the SSH server as part of graceful shutdown.
|
||||
sshSrv, err := agentssh.NewServer(a.hardCtx, a.logger.Named("ssh-server"), a.prometheusRegistry, a.filesystem, a.execer, &agentssh.Config{
|
||||
MaxTimeout: a.sshMaxTimeout,
|
||||
MOTDFile: func() string { return a.manifest.Load().MOTDFile },
|
||||
AnnouncementBanners: func() *[]codersdk.BannerConfig { return a.announcementBanners.Load() },
|
||||
UpdateEnv: a.updateCommandEnv,
|
||||
WorkingDirectory: func() string { return a.manifest.Load().Directory },
|
||||
BlockFileTransfer: a.blockFileTransfer,
|
||||
MaxTimeout: a.sshMaxTimeout,
|
||||
MOTDFile: func() string { return a.manifest.Load().MOTDFile },
|
||||
AnnouncementBanners: func() *[]codersdk.BannerConfig { return a.announcementBanners.Load() },
|
||||
UpdateEnv: a.updateCommandEnv,
|
||||
WorkingDirectory: func() string { return a.manifest.Load().Directory },
|
||||
BlockFileTransfer: a.blockFileTransfer,
|
||||
BlockReversePortForwarding: a.blockReversePortForwarding,
|
||||
BlockLocalPortForwarding: a.blockLocalPortForwarding,
|
||||
ReportConnection: func(id uuid.UUID, magicType agentssh.MagicSessionType, ip string) func(code int, reason string) {
|
||||
var connectionType proto.Connection_Type
|
||||
switch magicType {
|
||||
@@ -396,11 +406,17 @@ func (a *agent) init() {
|
||||
gitOpts := append([]agentgit.Option{agentgit.WithClock(a.clock)}, a.gitAPIOptions...)
|
||||
a.gitAPI = agentgit.NewAPI(a.logger.Named("git"), pathStore, gitOpts...)
|
||||
desktop := agentdesktop.NewPortableDesktop(
|
||||
a.logger.Named("desktop"), a.execer, a.scriptRunner.ScriptBinDir(),
|
||||
a.logger.Named("desktop"), a.execer, a.scriptRunner.ScriptBinDir(), nil,
|
||||
)
|
||||
a.desktopAPI = agentdesktop.NewAPI(a.logger.Named("desktop"), desktop, a.clock)
|
||||
a.mcpManager = agentmcp.NewManager(a.logger.Named("mcp"))
|
||||
a.mcpAPI = agentmcp.NewAPI(a.logger.Named("mcp"), a.mcpManager)
|
||||
a.contextConfigAPI = agentcontextconfig.NewAPI(func() string {
|
||||
if m := a.manifest.Load(); m != nil {
|
||||
return m.Directory
|
||||
}
|
||||
return ""
|
||||
})
|
||||
a.reconnectingPTYServer = reconnectingpty.NewServer(
|
||||
a.logger.Named("reconnecting-pty"),
|
||||
a.sshServer,
|
||||
@@ -1358,7 +1374,7 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
|
||||
// lifecycle transition to avoid delaying Ready.
|
||||
// This runs inside the tracked goroutine so it
|
||||
// is properly awaited on shutdown.
|
||||
if mcpErr := a.mcpManager.Connect(a.gracefulCtx, manifest.Directory); mcpErr != nil {
|
||||
if mcpErr := a.mcpManager.Connect(a.gracefulCtx, a.contextConfigAPI.MCPConfigFiles()); mcpErr != nil {
|
||||
a.logger.Warn(ctx, "failed to connect to workspace MCP servers", slog.Error(mcpErr))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -8,10 +10,22 @@ import (
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"cdr.dev/slog/v3/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/agent/agentcontextconfig"
|
||||
"github.com/coder/coder/v2/agent/proto"
|
||||
agentsdk "github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
// platformAbsPath constructs an absolute path that is valid
|
||||
// on the current platform. On Windows, paths must include a
|
||||
// drive letter to be considered absolute.
|
||||
func platformAbsPath(parts ...string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return `C:\` + filepath.Join(parts...)
|
||||
}
|
||||
return "/" + filepath.Join(parts...)
|
||||
}
|
||||
|
||||
// TestReportConnectionEmpty tests that reportConnection() doesn't choke if given an empty IP string, which is what we
|
||||
// send if we cannot get the remote address.
|
||||
func TestReportConnectionEmpty(t *testing.T) {
|
||||
@@ -42,3 +56,41 @@ func TestReportConnectionEmpty(t *testing.T) {
|
||||
require.Equal(t, proto.Connection_DISCONNECT, req1.GetConnection().GetAction())
|
||||
require.Equal(t, "because", req1.GetConnection().GetReason())
|
||||
}
|
||||
|
||||
func TestContextConfigAPI_InitOnce(t *testing.T) {
|
||||
// Not parallel: uses t.Setenv to clear env vars.
|
||||
|
||||
// Clear env vars so defaults are used and the test is
|
||||
// hermetic regardless of the surrounding environment.
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
// After the fix, contextConfigAPI is set once in init() and
|
||||
// never reassigned. Config() evaluates lazily via the
|
||||
// manifest, so there is no concurrent write to race with.
|
||||
dir1 := platformAbsPath("dir1")
|
||||
dir2 := platformAbsPath("dir2")
|
||||
|
||||
a := &agent{}
|
||||
a.manifest.Store(&agentsdk.Manifest{Directory: dir1})
|
||||
a.contextConfigAPI = agentcontextconfig.NewAPI(func() string {
|
||||
if m := a.manifest.Load(); m != nil {
|
||||
return m.Directory
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
mcpFiles1 := a.contextConfigAPI.MCPConfigFiles()
|
||||
require.NotEmpty(t, mcpFiles1)
|
||||
require.Contains(t, mcpFiles1[0], dir1)
|
||||
|
||||
// Simulate manifest update on reconnection -- no field
|
||||
// reassignment needed, the lazy closure picks it up.
|
||||
a.manifest.Store(&agentsdk.Manifest{Directory: dir2})
|
||||
mcpFiles2 := a.contextConfigAPI.MCPConfigFiles()
|
||||
require.NotEmpty(t, mcpFiles2)
|
||||
require.Contains(t, mcpFiles2[0], dir2)
|
||||
}
|
||||
|
||||
+170
-8
@@ -986,6 +986,161 @@ func TestAgent_TCPRemoteForwarding(t *testing.T) {
|
||||
requireEcho(t, conn)
|
||||
}
|
||||
|
||||
func TestAgent_TCPLocalForwardingBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
rl, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer rl.Close()
|
||||
tcpAddr, valid := rl.Addr().(*net.TCPAddr)
|
||||
require.True(t, valid)
|
||||
remotePort := tcpAddr.Port
|
||||
|
||||
//nolint:dogsled
|
||||
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
|
||||
o.BlockLocalPortForwarding = true
|
||||
})
|
||||
sshClient, err := agentConn.SSHClient(ctx)
|
||||
require.NoError(t, err)
|
||||
defer sshClient.Close()
|
||||
|
||||
_, err = sshClient.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", remotePort))
|
||||
require.ErrorContains(t, err, "administratively prohibited")
|
||||
}
|
||||
|
||||
func TestAgent_TCPRemoteForwardingBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
//nolint:dogsled
|
||||
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
|
||||
o.BlockReversePortForwarding = true
|
||||
})
|
||||
sshClient, err := agentConn.SSHClient(ctx)
|
||||
require.NoError(t, err)
|
||||
defer sshClient.Close()
|
||||
|
||||
localhost := netip.MustParseAddr("127.0.0.1")
|
||||
randomPort := testutil.RandomPortNoListen(t)
|
||||
addr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(localhost, randomPort))
|
||||
_, err = sshClient.ListenTCP(addr)
|
||||
require.ErrorContains(t, err, "tcpip-forward request denied by peer")
|
||||
}
|
||||
|
||||
func TestAgent_UnixLocalForwardingBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("unix domain sockets are not fully supported on Windows")
|
||||
}
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
tmpdir := testutil.TempDirUnixSocket(t)
|
||||
remoteSocketPath := filepath.Join(tmpdir, "remote-socket")
|
||||
|
||||
l, err := net.Listen("unix", remoteSocketPath)
|
||||
require.NoError(t, err)
|
||||
defer l.Close()
|
||||
|
||||
//nolint:dogsled
|
||||
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
|
||||
o.BlockLocalPortForwarding = true
|
||||
})
|
||||
sshClient, err := agentConn.SSHClient(ctx)
|
||||
require.NoError(t, err)
|
||||
defer sshClient.Close()
|
||||
|
||||
_, err = sshClient.Dial("unix", remoteSocketPath)
|
||||
require.ErrorContains(t, err, "administratively prohibited")
|
||||
}
|
||||
|
||||
func TestAgent_UnixRemoteForwardingBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("unix domain sockets are not fully supported on Windows")
|
||||
}
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
tmpdir := testutil.TempDirUnixSocket(t)
|
||||
remoteSocketPath := filepath.Join(tmpdir, "remote-socket")
|
||||
|
||||
//nolint:dogsled
|
||||
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
|
||||
o.BlockReversePortForwarding = true
|
||||
})
|
||||
sshClient, err := agentConn.SSHClient(ctx)
|
||||
require.NoError(t, err)
|
||||
defer sshClient.Close()
|
||||
|
||||
_, err = sshClient.ListenUnix(remoteSocketPath)
|
||||
require.ErrorContains(t, err, "streamlocal-forward@openssh.com request denied by peer")
|
||||
}
|
||||
|
||||
// TestAgent_LocalBlockedDoesNotAffectReverse verifies that blocking
|
||||
// local port forwarding does not prevent reverse port forwarding from
|
||||
// working. A field-name transposition at any plumbing hop would cause
|
||||
// both directions to be blocked when only one flag is set.
|
||||
func TestAgent_LocalBlockedDoesNotAffectReverse(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
//nolint:dogsled
|
||||
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
|
||||
o.BlockLocalPortForwarding = true
|
||||
})
|
||||
sshClient, err := agentConn.SSHClient(ctx)
|
||||
require.NoError(t, err)
|
||||
defer sshClient.Close()
|
||||
|
||||
// Reverse forwarding must still work.
|
||||
localhost := netip.MustParseAddr("127.0.0.1")
|
||||
var ll net.Listener
|
||||
for {
|
||||
randomPort := testutil.RandomPortNoListen(t)
|
||||
addr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(localhost, randomPort))
|
||||
ll, err = sshClient.ListenTCP(addr)
|
||||
if err != nil {
|
||||
t.Logf("error remote forwarding: %s", err.Error())
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timed out getting random listener")
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
_ = ll.Close()
|
||||
}
|
||||
|
||||
// TestAgent_ReverseBlockedDoesNotAffectLocal verifies that blocking
|
||||
// reverse port forwarding does not prevent local port forwarding from
|
||||
// working.
|
||||
func TestAgent_ReverseBlockedDoesNotAffectLocal(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
rl, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer rl.Close()
|
||||
tcpAddr, valid := rl.Addr().(*net.TCPAddr)
|
||||
require.True(t, valid)
|
||||
remotePort := tcpAddr.Port
|
||||
go echoOnce(t, rl)
|
||||
|
||||
//nolint:dogsled
|
||||
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
|
||||
o.BlockReversePortForwarding = true
|
||||
})
|
||||
sshClient, err := agentConn.SSHClient(ctx)
|
||||
require.NoError(t, err)
|
||||
defer sshClient.Close()
|
||||
|
||||
// Local forwarding must still work.
|
||||
conn, err := sshClient.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", remotePort))
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
requireEcho(t, conn)
|
||||
}
|
||||
|
||||
func TestAgent_UnixLocalForwarding(t *testing.T) {
|
||||
t.Parallel()
|
||||
if runtime.GOOS == "windows" {
|
||||
@@ -3007,7 +3162,7 @@ func TestAgent_Speedtest(t *testing.T) {
|
||||
|
||||
func TestAgent_Reconnect(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
logger := testutil.Logger(t)
|
||||
// After the agent is disconnected from a coordinator, it's supposed
|
||||
// to reconnect!
|
||||
@@ -3020,7 +3175,8 @@ func TestAgent_Reconnect(t *testing.T) {
|
||||
logger,
|
||||
agentID,
|
||||
agentsdk.Manifest{
|
||||
DERPMap: derpMap,
|
||||
DERPMap: derpMap,
|
||||
Directory: "/test/workspace",
|
||||
},
|
||||
statsCh,
|
||||
fCoordinator,
|
||||
@@ -3033,13 +3189,19 @@ func TestAgent_Reconnect(t *testing.T) {
|
||||
})
|
||||
defer closer.Close()
|
||||
|
||||
call1 := testutil.RequireReceive(ctx, t, fCoordinator.CoordinateCalls)
|
||||
require.Equal(t, client.GetNumRefreshTokenCalls(), 1)
|
||||
close(call1.Resps) // hang up
|
||||
// expect reconnect
|
||||
// Each iteration forces the agent to reconnect by closing
|
||||
// the current coordinate call while the tracked HTTP server
|
||||
// goroutine (from connection 1's createTailnet) is still
|
||||
// alive, widening the race window.
|
||||
const reconnections = 5
|
||||
for i := range reconnections {
|
||||
call := testutil.RequireReceive(ctx, t, fCoordinator.CoordinateCalls)
|
||||
require.Equal(t, i+1, client.GetNumRefreshTokenCalls())
|
||||
close(call.Resps) // hang up — triggers reconnect
|
||||
}
|
||||
// Verify final reconnect succeeds.
|
||||
testutil.RequireReceive(ctx, t, fCoordinator.CoordinateCalls)
|
||||
// Check that the agent refreshes the token when it reconnects.
|
||||
require.Equal(t, client.GetNumRefreshTokenCalls(), 2)
|
||||
require.Equal(t, reconnections+1, client.GetNumRefreshTokenCalls())
|
||||
closer.Close()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
package agentcontextconfig
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
)
|
||||
|
||||
// Env var names for context configuration. Prefixed with EXP_
|
||||
// to indicate these are experimental and may change.
|
||||
const (
|
||||
EnvInstructionsDirs = "CODER_AGENT_EXP_INSTRUCTIONS_DIRS"
|
||||
EnvInstructionsFile = "CODER_AGENT_EXP_INSTRUCTIONS_FILE"
|
||||
EnvSkillsDirs = "CODER_AGENT_EXP_SKILLS_DIRS"
|
||||
EnvSkillMetaFile = "CODER_AGENT_EXP_SKILL_META_FILE"
|
||||
EnvMCPConfigFiles = "CODER_AGENT_EXP_MCP_CONFIG_FILES"
|
||||
)
|
||||
|
||||
const (
|
||||
maxInstructionFileBytes = 64 * 1024
|
||||
maxSkillMetaBytes = 64 * 1024
|
||||
)
|
||||
|
||||
// markdownCommentPattern strips HTML comments from instruction
|
||||
// file content for security (prevents hidden prompt injection).
|
||||
var markdownCommentPattern = regexp.MustCompile(`<!--[\s\S]*?-->`)
|
||||
|
||||
// invisibleRunePattern strips invisible Unicode characters that
|
||||
// could be used for prompt injection.
|
||||
//
|
||||
//nolint:gocritic // Non-ASCII char ranges are intentional for invisible Unicode stripping.
|
||||
var invisibleRunePattern = regexp.MustCompile(
|
||||
"[\u00ad\u034f\u061c\u070f" +
|
||||
"\u115f\u1160\u17b4\u17b5" +
|
||||
"\u180b-\u180f" +
|
||||
"\u200b\u200d\u200e\u200f" +
|
||||
"\u202a-\u202e" +
|
||||
"\u2060-\u206f" +
|
||||
"\u3164" +
|
||||
"\ufe00-\ufe0f" +
|
||||
"\ufeff" +
|
||||
"\uffa0" +
|
||||
"\ufff0-\ufff8]",
|
||||
)
|
||||
|
||||
// skillNamePattern validates kebab-case skill names.
|
||||
var skillNamePattern = regexp.MustCompile(
|
||||
`^[a-z0-9]+(-[a-z0-9]+)*$`,
|
||||
)
|
||||
|
||||
// Default values for agent-internal configuration. These are
|
||||
// used when the corresponding env vars are unset.
|
||||
const (
|
||||
DefaultInstructionsDir = "~/.coder"
|
||||
DefaultInstructionsFile = "AGENTS.md"
|
||||
DefaultSkillsDir = ".agents/skills"
|
||||
DefaultSkillMetaFile = "SKILL.md"
|
||||
DefaultMCPConfigFile = ".mcp.json"
|
||||
)
|
||||
|
||||
// API exposes the resolved context configuration through the
|
||||
// agent's HTTP API.
|
||||
type API struct {
|
||||
workingDir func() string
|
||||
}
|
||||
|
||||
// NewAPI accepts a closure that returns the working directory.
|
||||
// The directory is evaluated lazily on each call to Config(),
|
||||
// so the caller can update it after construction.
|
||||
func NewAPI(workingDir func() string) *API {
|
||||
if workingDir == nil {
|
||||
workingDir = func() string { return "" }
|
||||
}
|
||||
return &API{workingDir: workingDir}
|
||||
}
|
||||
|
||||
// Config reads env vars, resolves paths, reads instruction files,
|
||||
// and discovers skills. Returns the HTTP response and the resolved
|
||||
// MCP config file paths (used only agent-internally). Exported
|
||||
// for use by tests.
|
||||
func Config(workingDir string) (workspacesdk.ContextConfigResponse, []string) {
|
||||
// TrimSpace all env vars before cmp.Or so that a
|
||||
// whitespace-only value falls through to the default
|
||||
// consistently. ResolvePaths also trims each comma-
|
||||
// separated entry, but without pre-trimming here a
|
||||
// bare " " would bypass cmp.Or and produce nil.
|
||||
instructionsDir := cmp.Or(strings.TrimSpace(os.Getenv(EnvInstructionsDirs)), DefaultInstructionsDir)
|
||||
instructionsFile := cmp.Or(strings.TrimSpace(os.Getenv(EnvInstructionsFile)), DefaultInstructionsFile)
|
||||
skillsDir := cmp.Or(strings.TrimSpace(os.Getenv(EnvSkillsDirs)), DefaultSkillsDir)
|
||||
skillMetaFile := cmp.Or(strings.TrimSpace(os.Getenv(EnvSkillMetaFile)), DefaultSkillMetaFile)
|
||||
mcpConfigFile := cmp.Or(strings.TrimSpace(os.Getenv(EnvMCPConfigFiles)), DefaultMCPConfigFile)
|
||||
|
||||
resolvedInstructionsDirs := ResolvePaths(instructionsDir, workingDir)
|
||||
resolvedSkillsDirs := ResolvePaths(skillsDir, workingDir)
|
||||
|
||||
// Read instruction files from each configured directory.
|
||||
parts := readInstructionFiles(resolvedInstructionsDirs, instructionsFile)
|
||||
|
||||
// Also check the working directory for the instruction file,
|
||||
// unless it was already covered by InstructionsDirs.
|
||||
if workingDir != "" {
|
||||
seenDirs := make(map[string]struct{}, len(resolvedInstructionsDirs))
|
||||
for _, d := range resolvedInstructionsDirs {
|
||||
seenDirs[d] = struct{}{}
|
||||
}
|
||||
if _, ok := seenDirs[workingDir]; !ok {
|
||||
if entry, found := readInstructionFileFromDir(workingDir, instructionsFile); found {
|
||||
parts = append(parts, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discover skills from each configured skills directory.
|
||||
skillParts := discoverSkills(resolvedSkillsDirs, skillMetaFile)
|
||||
parts = append(parts, skillParts...)
|
||||
|
||||
// Guarantee non-nil slice to signal agent support.
|
||||
if parts == nil {
|
||||
parts = []codersdk.ChatMessagePart{}
|
||||
}
|
||||
|
||||
return workspacesdk.ContextConfigResponse{
|
||||
Parts: parts,
|
||||
}, ResolvePaths(mcpConfigFile, workingDir)
|
||||
}
|
||||
|
||||
// MCPConfigFiles returns the resolved MCP configuration file
|
||||
// paths for the agent's MCP manager.
|
||||
func (api *API) MCPConfigFiles() []string {
|
||||
_, mcpFiles := Config(api.workingDir())
|
||||
return mcpFiles
|
||||
}
|
||||
|
||||
// Routes returns the HTTP handler for the context config
|
||||
// endpoint.
|
||||
func (api *API) Routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/", api.handleGet)
|
||||
return r
|
||||
}
|
||||
|
||||
func (api *API) handleGet(rw http.ResponseWriter, r *http.Request) {
|
||||
response, _ := Config(api.workingDir())
|
||||
httpapi.Write(r.Context(), rw, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// readInstructionFiles reads instruction files from each given
|
||||
// directory. Missing directories are silently skipped. Duplicate
|
||||
// directories are deduplicated.
|
||||
func readInstructionFiles(dirs []string, fileName string) []codersdk.ChatMessagePart {
|
||||
var parts []codersdk.ChatMessagePart
|
||||
seen := make(map[string]struct{}, len(dirs))
|
||||
for _, dir := range dirs {
|
||||
if _, ok := seen[dir]; ok {
|
||||
continue
|
||||
}
|
||||
seen[dir] = struct{}{}
|
||||
if part, found := readInstructionFileFromDir(dir, fileName); found {
|
||||
parts = append(parts, part)
|
||||
}
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
// readInstructionFileFromDir scans a directory for a file matching
|
||||
// fileName (case-insensitive) and reads its contents.
|
||||
func readInstructionFileFromDir(dir, fileName string) (codersdk.ChatMessagePart, bool) {
|
||||
dirEntries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return codersdk.ChatMessagePart{}, false
|
||||
}
|
||||
|
||||
for _, e := range dirEntries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(e.Name()), fileName) {
|
||||
filePath := filepath.Join(dir, e.Name())
|
||||
content, truncated, ok := readAndSanitizeFile(filePath, maxInstructionFileBytes)
|
||||
if !ok {
|
||||
return codersdk.ChatMessagePart{}, false
|
||||
}
|
||||
if content == "" {
|
||||
return codersdk.ChatMessagePart{}, false
|
||||
}
|
||||
return codersdk.ChatMessagePart{
|
||||
Type: codersdk.ChatMessagePartTypeContextFile,
|
||||
ContextFilePath: filePath,
|
||||
ContextFileContent: content,
|
||||
ContextFileTruncated: truncated,
|
||||
}, true
|
||||
}
|
||||
}
|
||||
return codersdk.ChatMessagePart{}, false
|
||||
}
|
||||
|
||||
// readAndSanitizeFile reads the file at path, capping the read
|
||||
// at maxBytes to avoid unbounded memory allocation. It sanitizes
|
||||
// the content (strips HTML comments and invisible Unicode) and
|
||||
// returns the result. Returns false if the file cannot be read.
|
||||
func readAndSanitizeFile(path string, maxBytes int64) (content string, truncated bool, ok bool) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", false, false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Read at most maxBytes+1 to detect truncation without
|
||||
// allocating the entire file into memory.
|
||||
raw, err := io.ReadAll(io.LimitReader(f, maxBytes+1))
|
||||
if err != nil {
|
||||
return "", false, false
|
||||
}
|
||||
|
||||
truncated = int64(len(raw)) > maxBytes
|
||||
if truncated {
|
||||
raw = raw[:maxBytes]
|
||||
}
|
||||
|
||||
s := sanitizeInstructionMarkdown(string(raw))
|
||||
if s == "" {
|
||||
return "", truncated, true
|
||||
}
|
||||
return s, truncated, true
|
||||
}
|
||||
|
||||
// sanitizeInstructionMarkdown strips HTML comments, invisible
|
||||
// Unicode characters, and CRLF line endings from instruction
|
||||
// file content.
|
||||
func sanitizeInstructionMarkdown(content string) string {
|
||||
content = strings.ReplaceAll(content, "\r\n", "\n")
|
||||
content = strings.ReplaceAll(content, "\r", "\n")
|
||||
content = markdownCommentPattern.ReplaceAllString(content, "")
|
||||
content = invisibleRunePattern.ReplaceAllString(content, "")
|
||||
return strings.TrimSpace(content)
|
||||
}
|
||||
|
||||
// discoverSkills walks the given skills directories and returns
|
||||
// metadata for every valid skill it finds. Body and supporting
|
||||
// file lists are NOT included; chatd fetches those on demand
|
||||
// via read_skill. Missing directories or individual errors are
|
||||
// silently skipped.
|
||||
func discoverSkills(skillsDirs []string, metaFile string) []codersdk.ChatMessagePart {
|
||||
seen := make(map[string]struct{})
|
||||
var parts []codersdk.ChatMessagePart
|
||||
|
||||
for _, skillsDir := range skillsDirs {
|
||||
entries, err := os.ReadDir(skillsDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
metaPath := filepath.Join(skillsDir, entry.Name(), metaFile)
|
||||
f, err := os.Open(metaPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
raw, err := io.ReadAll(io.LimitReader(f, maxSkillMetaBytes+1))
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if int64(len(raw)) > maxSkillMetaBytes {
|
||||
raw = raw[:maxSkillMetaBytes]
|
||||
}
|
||||
|
||||
name, description, _, err := workspacesdk.ParseSkillFrontmatter(string(raw))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// The directory name must match the declared name.
|
||||
if name != entry.Name() {
|
||||
continue
|
||||
}
|
||||
if !skillNamePattern.MatchString(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// First occurrence wins across directories.
|
||||
if _, ok := seen[name]; ok {
|
||||
continue
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
|
||||
skillDir := filepath.Join(skillsDir, entry.Name())
|
||||
parts = append(parts, codersdk.ChatMessagePart{
|
||||
Type: codersdk.ChatMessagePartTypeSkill,
|
||||
SkillName: name,
|
||||
SkillDescription: description,
|
||||
SkillDir: skillDir,
|
||||
ContextFileSkillMetaFile: metaFile,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
package agentcontextconfig_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentcontextconfig"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// filterParts returns only the parts matching the given type.
|
||||
func filterParts(parts []codersdk.ChatMessagePart, t codersdk.ChatMessagePartType) []codersdk.ChatMessagePart {
|
||||
var out []codersdk.ChatMessagePart
|
||||
for _, p := range parts {
|
||||
if p.Type == t {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
|
||||
// Clear all env vars so defaults are used.
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
workDir := platformAbsPath("work")
|
||||
cfg, mcpFiles := agentcontextconfig.Config(workDir)
|
||||
|
||||
// Parts is always non-nil.
|
||||
require.NotNil(t, cfg.Parts)
|
||||
// Default MCP config file is ".mcp.json" (relative),
|
||||
// resolved against the working directory.
|
||||
require.Equal(t, []string{filepath.Join(workDir, ".mcp.json")}, mcpFiles)
|
||||
})
|
||||
|
||||
t.Run("CustomEnvVars", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
|
||||
optInstructions := t.TempDir()
|
||||
optSkills := t.TempDir()
|
||||
optMCP := platformAbsPath("opt", "mcp.json")
|
||||
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, optInstructions)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "CUSTOM.md")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, optSkills)
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "META.yaml")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, optMCP)
|
||||
|
||||
// Create files matching the custom names so we can
|
||||
// verify the env vars actually change lookup behavior.
|
||||
require.NoError(t, os.WriteFile(filepath.Join(optInstructions, "CUSTOM.md"), []byte("custom instructions"), 0o600))
|
||||
skillDir := filepath.Join(optSkills, "my-skill")
|
||||
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(skillDir, "META.yaml"),
|
||||
[]byte("---\nname: my-skill\ndescription: custom meta\n---\n"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
workDir := platformAbsPath("work")
|
||||
cfg, mcpFiles := agentcontextconfig.Config(workDir)
|
||||
|
||||
require.Equal(t, []string{optMCP}, mcpFiles)
|
||||
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
|
||||
require.Len(t, ctxFiles, 1)
|
||||
require.Equal(t, "custom instructions", ctxFiles[0].ContextFileContent)
|
||||
skillParts := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeSkill)
|
||||
require.Len(t, skillParts, 1)
|
||||
require.Equal(t, "my-skill", skillParts[0].SkillName)
|
||||
require.Equal(t, "META.yaml", skillParts[0].ContextFileSkillMetaFile)
|
||||
})
|
||||
|
||||
t.Run("WhitespaceInFileNames", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, " CLAUDE.md ")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
workDir := t.TempDir()
|
||||
// Create a file matching the trimmed name.
|
||||
require.NoError(t, os.WriteFile(filepath.Join(fakeHome, "CLAUDE.md"), []byte("hello"), 0o600))
|
||||
|
||||
cfg, _ := agentcontextconfig.Config(workDir)
|
||||
|
||||
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
|
||||
require.Len(t, ctxFiles, 1)
|
||||
require.Equal(t, "hello", ctxFiles[0].ContextFileContent)
|
||||
})
|
||||
|
||||
t.Run("CommaSeparatedDirs", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
|
||||
a := t.TempDir()
|
||||
b := t.TempDir()
|
||||
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, a+","+b)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
// Put instruction files in both dirs.
|
||||
require.NoError(t, os.WriteFile(filepath.Join(a, "AGENTS.md"), []byte("from a"), 0o600))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(b, "AGENTS.md"), []byte("from b"), 0o600))
|
||||
|
||||
workDir := t.TempDir()
|
||||
cfg, _ := agentcontextconfig.Config(workDir)
|
||||
|
||||
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
|
||||
require.Len(t, ctxFiles, 2)
|
||||
require.Equal(t, "from a", ctxFiles[0].ContextFileContent)
|
||||
require.Equal(t, "from b", ctxFiles[1].ContextFileContent)
|
||||
})
|
||||
|
||||
t.Run("ReadsInstructionFiles", func(t *testing.T) {
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
workDir := t.TempDir()
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
|
||||
// Create ~/.coder/AGENTS.md
|
||||
coderDir := filepath.Join(fakeHome, ".coder")
|
||||
require.NoError(t, os.MkdirAll(coderDir, 0o755))
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(coderDir, "AGENTS.md"),
|
||||
[]byte("home instructions"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
cfg, _ := agentcontextconfig.Config(workDir)
|
||||
|
||||
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
|
||||
require.NotNil(t, cfg.Parts)
|
||||
require.Len(t, ctxFiles, 1)
|
||||
require.Equal(t, "home instructions", ctxFiles[0].ContextFileContent)
|
||||
require.Equal(t, filepath.Join(coderDir, "AGENTS.md"), ctxFiles[0].ContextFilePath)
|
||||
require.False(t, ctxFiles[0].ContextFileTruncated)
|
||||
})
|
||||
|
||||
t.Run("ReadsWorkingDirInstructionFile", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
workDir := t.TempDir()
|
||||
|
||||
// Create AGENTS.md in the working directory.
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(workDir, "AGENTS.md"),
|
||||
[]byte("project instructions"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
cfg, _ := agentcontextconfig.Config(workDir)
|
||||
|
||||
// Should find the working dir file (not in instruction dirs).
|
||||
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
|
||||
require.NotNil(t, cfg.Parts)
|
||||
require.Len(t, ctxFiles, 1)
|
||||
require.Equal(t, "project instructions", ctxFiles[0].ContextFileContent)
|
||||
require.Equal(t, filepath.Join(workDir, "AGENTS.md"), ctxFiles[0].ContextFilePath)
|
||||
})
|
||||
|
||||
t.Run("TruncatesLargeInstructionFile", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
workDir := t.TempDir()
|
||||
largeContent := strings.Repeat("a", 64*1024+100)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte(largeContent), 0o600))
|
||||
|
||||
cfg, _ := agentcontextconfig.Config(workDir)
|
||||
|
||||
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
|
||||
require.Len(t, ctxFiles, 1)
|
||||
require.True(t, ctxFiles[0].ContextFileTruncated)
|
||||
require.Len(t, ctxFiles[0].ContextFileContent, 64*1024)
|
||||
})
|
||||
|
||||
t.Run("SanitizesHTMLComments", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
workDir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(workDir, "AGENTS.md"),
|
||||
[]byte("visible\n<!-- hidden -->content"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
cfg, _ := agentcontextconfig.Config(workDir)
|
||||
|
||||
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
|
||||
require.Len(t, ctxFiles, 1)
|
||||
require.Equal(t, "visible\ncontent", ctxFiles[0].ContextFileContent)
|
||||
})
|
||||
|
||||
t.Run("SanitizesInvisibleUnicode", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
workDir := t.TempDir()
|
||||
// U+200B (zero-width space) should be stripped.
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(workDir, "AGENTS.md"),
|
||||
[]byte("before\u200bafter"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
cfg, _ := agentcontextconfig.Config(workDir)
|
||||
|
||||
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
|
||||
require.Len(t, ctxFiles, 1)
|
||||
require.Equal(t, "beforeafter", ctxFiles[0].ContextFileContent)
|
||||
})
|
||||
|
||||
t.Run("NormalizesCRLF", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
workDir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(workDir, "AGENTS.md"),
|
||||
[]byte("line1\r\nline2\rline3"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
cfg, _ := agentcontextconfig.Config(workDir)
|
||||
|
||||
ctxFiles := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeContextFile)
|
||||
require.Len(t, ctxFiles, 1)
|
||||
require.Equal(t, "line1\nline2\nline3", ctxFiles[0].ContextFileContent)
|
||||
})
|
||||
|
||||
t.Run("DiscoversSkills", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
workDir := t.TempDir()
|
||||
skillsDir := filepath.Join(workDir, ".agents", "skills")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, skillsDir)
|
||||
|
||||
// Create a valid skill.
|
||||
skillDir := filepath.Join(skillsDir, "my-skill")
|
||||
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(skillDir, "SKILL.md"),
|
||||
[]byte("---\nname: my-skill\ndescription: A test skill\n---\nSkill body"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
cfg, _ := agentcontextconfig.Config(workDir)
|
||||
|
||||
skillParts := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeSkill)
|
||||
require.Len(t, skillParts, 1)
|
||||
require.Equal(t, "my-skill", skillParts[0].SkillName)
|
||||
require.Equal(t, "A test skill", skillParts[0].SkillDescription)
|
||||
require.Equal(t, skillDir, skillParts[0].SkillDir)
|
||||
require.Equal(t, "SKILL.md", skillParts[0].ContextFileSkillMetaFile)
|
||||
})
|
||||
|
||||
t.Run("SkipsMissingDirs", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
|
||||
nonExistent := filepath.Join(t.TempDir(), "does-not-exist")
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, nonExistent)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, nonExistent)
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
workDir := t.TempDir()
|
||||
cfg, _ := agentcontextconfig.Config(workDir)
|
||||
|
||||
// Non-nil empty slice (signals agent supports new format).
|
||||
require.NotNil(t, cfg.Parts)
|
||||
require.Empty(t, cfg.Parts)
|
||||
})
|
||||
|
||||
t.Run("MCPConfigFilesResolvedSeparately", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
|
||||
optMCP := platformAbsPath("opt", "custom.json")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, optMCP)
|
||||
|
||||
workDir := t.TempDir()
|
||||
_, mcpFiles := agentcontextconfig.Config(workDir)
|
||||
|
||||
require.Equal(t, []string{optMCP}, mcpFiles)
|
||||
})
|
||||
|
||||
t.Run("SkillNameMustMatchDir", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
workDir := t.TempDir()
|
||||
skillsDir := filepath.Join(workDir, "skills")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, skillsDir)
|
||||
|
||||
// Skill name in frontmatter doesn't match directory name.
|
||||
skillDir := filepath.Join(skillsDir, "wrong-dir-name")
|
||||
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(skillDir, "SKILL.md"),
|
||||
[]byte("---\nname: actual-name\ndescription: mismatch\n---\n"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
cfg, _ := agentcontextconfig.Config(workDir)
|
||||
skillParts := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeSkill)
|
||||
require.Empty(t, skillParts)
|
||||
})
|
||||
|
||||
t.Run("DuplicateSkillsFirstWins", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, fakeHome)
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
workDir := t.TempDir()
|
||||
skillsDir1 := filepath.Join(workDir, "skills1")
|
||||
skillsDir2 := filepath.Join(workDir, "skills2")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, skillsDir1+","+skillsDir2)
|
||||
|
||||
// Same skill name in both directories.
|
||||
for _, dir := range []string{skillsDir1, skillsDir2} {
|
||||
skillDir := filepath.Join(dir, "dup-skill")
|
||||
require.NoError(t, os.MkdirAll(skillDir, 0o755))
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(skillDir, "SKILL.md"),
|
||||
[]byte("---\nname: dup-skill\ndescription: from "+filepath.Base(dir)+"\n---\n"),
|
||||
0o600,
|
||||
))
|
||||
}
|
||||
|
||||
cfg, _ := agentcontextconfig.Config(workDir)
|
||||
skillParts := filterParts(cfg.Parts, codersdk.ChatMessagePartTypeSkill)
|
||||
require.Len(t, skillParts, 1)
|
||||
require.Equal(t, "from skills1", skillParts[0].SkillDescription)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewAPI_LazyDirectory(t *testing.T) {
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvInstructionsFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillsDirs, "")
|
||||
t.Setenv(agentcontextconfig.EnvSkillMetaFile, "")
|
||||
t.Setenv(agentcontextconfig.EnvMCPConfigFiles, "")
|
||||
|
||||
dir := ""
|
||||
api := agentcontextconfig.NewAPI(func() string { return dir })
|
||||
|
||||
// Before directory is set, MCP paths resolve to nothing.
|
||||
mcpFiles := api.MCPConfigFiles()
|
||||
require.Empty(t, mcpFiles)
|
||||
|
||||
// After setting the directory, MCPConfigFiles() picks it up.
|
||||
dir = platformAbsPath("work")
|
||||
mcpFiles = api.MCPConfigFiles()
|
||||
require.NotEmpty(t, mcpFiles)
|
||||
require.Equal(t, []string{filepath.Join(dir, ".mcp.json")}, mcpFiles)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package agentcontextconfig
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ResolvePath resolves a single path that may be absolute,
|
||||
// home-relative (~/ or ~), or relative to the given base
|
||||
// directory. Returns an absolute path. Empty input returns empty.
|
||||
func ResolvePath(raw, baseDir string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
switch {
|
||||
case raw == "~":
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return home
|
||||
case strings.HasPrefix(raw, "~/"):
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(home, raw[2:])
|
||||
case filepath.IsAbs(raw):
|
||||
return raw
|
||||
default:
|
||||
if baseDir == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(baseDir, raw)
|
||||
}
|
||||
}
|
||||
|
||||
// ResolvePaths splits a comma-separated list of paths and
|
||||
// resolves each entry independently. Empty entries and entries
|
||||
// that resolve to empty strings are skipped.
|
||||
func ResolvePaths(raw, baseDir string) []string {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if resolved := ResolvePath(p, baseDir); resolved != "" {
|
||||
out = append(out, resolved)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package agentcontextconfig_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentcontextconfig"
|
||||
)
|
||||
|
||||
// platformAbsPath constructs an absolute path that is valid
|
||||
// on the current platform. On Windows paths must include a
|
||||
// drive letter to be considered absolute.
|
||||
func platformAbsPath(parts ...string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return `C:\` + filepath.Join(parts...)
|
||||
}
|
||||
return "/" + filepath.Join(parts...)
|
||||
}
|
||||
|
||||
func TestResolvePath(t *testing.T) { //nolint:tparallel // subtests using t.Setenv cannot be parallel
|
||||
t.Run("EmptyInput", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, "", agentcontextconfig.ResolvePath("", platformAbsPath("base")))
|
||||
})
|
||||
|
||||
t.Run("WhitespaceOnly", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, "", agentcontextconfig.ResolvePath(" ", platformAbsPath("base")))
|
||||
})
|
||||
|
||||
// Tests that use t.Setenv cannot be parallel.
|
||||
t.Run("TildeAlone", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
got := agentcontextconfig.ResolvePath("~", platformAbsPath("base"))
|
||||
require.Equal(t, fakeHome, got)
|
||||
})
|
||||
|
||||
t.Run("TildeSlashPath", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
got := agentcontextconfig.ResolvePath("~/docs/readme", platformAbsPath("base"))
|
||||
require.Equal(t, filepath.Join(fakeHome, "docs", "readme"), got)
|
||||
})
|
||||
|
||||
t.Run("AbsolutePath", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := platformAbsPath("etc", "coder")
|
||||
got := agentcontextconfig.ResolvePath(p, platformAbsPath("base"))
|
||||
require.Equal(t, p, got)
|
||||
})
|
||||
|
||||
t.Run("RelativePath", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
base := platformAbsPath("work")
|
||||
got := agentcontextconfig.ResolvePath("foo/bar", base)
|
||||
require.Equal(t, filepath.Join(base, "foo", "bar"), got)
|
||||
})
|
||||
|
||||
t.Run("RelativePathWithWhitespace", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
base := platformAbsPath("work")
|
||||
got := agentcontextconfig.ResolvePath(" foo/bar ", base)
|
||||
require.Equal(t, filepath.Join(base, "foo", "bar"), got)
|
||||
})
|
||||
|
||||
t.Run("RelativePathWithEmptyBaseDir", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := agentcontextconfig.ResolvePath(".agents/skills", "")
|
||||
require.Equal(t, "", got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolvePath_HomeUnset(t *testing.T) {
|
||||
// Cannot be parallel — modifies HOME env var.
|
||||
t.Setenv("HOME", "")
|
||||
// Also clear USERPROFILE for Windows compatibility.
|
||||
t.Setenv("USERPROFILE", "")
|
||||
|
||||
require.Equal(t, "", agentcontextconfig.ResolvePath("~", platformAbsPath("base")))
|
||||
require.Equal(t, "", agentcontextconfig.ResolvePath("~/docs", platformAbsPath("base")))
|
||||
}
|
||||
|
||||
func TestResolvePaths(t *testing.T) { //nolint:tparallel // subtests using t.Setenv cannot be parallel
|
||||
t.Run("EmptyString", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Nil(t, agentcontextconfig.ResolvePaths("", platformAbsPath("base")))
|
||||
})
|
||||
|
||||
t.Run("WhitespaceOnly", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Nil(t, agentcontextconfig.ResolvePaths(" ", platformAbsPath("base")))
|
||||
})
|
||||
|
||||
t.Run("SingleEntry", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := platformAbsPath("abs", "path")
|
||||
got := agentcontextconfig.ResolvePaths(p, platformAbsPath("base"))
|
||||
require.Equal(t, []string{p}, got)
|
||||
})
|
||||
|
||||
// Tests that use t.Setenv cannot be parallel.
|
||||
t.Run("MultipleEntries", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
b := platformAbsPath("b")
|
||||
base := platformAbsPath("base")
|
||||
got := agentcontextconfig.ResolvePaths("~/a,"+b+",rel", base)
|
||||
require.Equal(t, []string{
|
||||
filepath.Join(fakeHome, "a"),
|
||||
b,
|
||||
filepath.Join(base, "rel"),
|
||||
}, got)
|
||||
})
|
||||
|
||||
t.Run("TrimsWhitespace", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := platformAbsPath("a")
|
||||
b := platformAbsPath("b")
|
||||
got := agentcontextconfig.ResolvePaths(" "+a+" , "+b+" ", platformAbsPath("base"))
|
||||
require.Equal(t, []string{a, b}, got)
|
||||
})
|
||||
|
||||
t.Run("SkipsEmptyEntries", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := platformAbsPath("a")
|
||||
b := platformAbsPath("b")
|
||||
got := agentcontextconfig.ResolvePaths(a+",,"+b+",", platformAbsPath("base"))
|
||||
require.Equal(t, []string{a, b}, got)
|
||||
})
|
||||
|
||||
t.Run("TrailingComma", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := platformAbsPath("only")
|
||||
got := agentcontextconfig.ResolvePaths(p+",", platformAbsPath("base"))
|
||||
require.Equal(t, []string{p}, got)
|
||||
})
|
||||
|
||||
t.Run("RelativePathSkippedWhenBaseDirEmpty", func(t *testing.T) {
|
||||
fakeHome := t.TempDir()
|
||||
t.Setenv("HOME", fakeHome)
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
got := agentcontextconfig.ResolvePaths("~/.coder,.agents/skills", "")
|
||||
require.Equal(t, []string{filepath.Join(fakeHome, ".coder")}, got)
|
||||
})
|
||||
}
|
||||
@@ -148,6 +148,11 @@ func (m *manager) start(req workspacesdk.StartProcessRequest, chatID string) (*p
|
||||
for k, v := range req.Env {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
// Propagate the chat ID so child processes (e.g.
|
||||
// GIT_ASKPASS) can send it back to the server.
|
||||
if chatID != "" {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("CODER_CHAT_ID=%s", chatID))
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
cancel()
|
||||
|
||||
@@ -117,6 +117,10 @@ type Config struct {
|
||||
X11MaxPort *int
|
||||
// BlockFileTransfer restricts use of file transfer applications.
|
||||
BlockFileTransfer bool
|
||||
// BlockReversePortForwarding disables reverse port forwarding (ssh -R).
|
||||
BlockReversePortForwarding bool
|
||||
// BlockLocalPortForwarding disables local port forwarding (ssh -L).
|
||||
BlockLocalPortForwarding bool
|
||||
// ReportConnection.
|
||||
ReportConnection reportConnectionFunc
|
||||
// Experimental: allow connecting to running containers via Docker exec.
|
||||
@@ -190,7 +194,7 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
|
||||
}
|
||||
|
||||
forwardHandler := &ssh.ForwardedTCPHandler{}
|
||||
unixForwardHandler := newForwardedUnixHandler(logger)
|
||||
unixForwardHandler := newForwardedUnixHandler(logger, config.BlockReversePortForwarding)
|
||||
|
||||
metrics := newSSHServerMetrics(prometheusRegistry)
|
||||
s := &Server{
|
||||
@@ -229,8 +233,15 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
|
||||
wrapped := NewJetbrainsChannelWatcher(ctx, s.logger, s.config.ReportConnection, newChan, &s.connCountJetBrains)
|
||||
ssh.DirectTCPIPHandler(srv, conn, wrapped, ctx)
|
||||
},
|
||||
"direct-streamlocal@openssh.com": directStreamLocalHandler,
|
||||
"session": ssh.DefaultSessionHandler,
|
||||
"direct-streamlocal@openssh.com": func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
|
||||
if s.config.BlockLocalPortForwarding {
|
||||
s.logger.Warn(ctx, "unix local port forward blocked")
|
||||
_ = newChan.Reject(gossh.Prohibited, "local port forwarding is disabled")
|
||||
return
|
||||
}
|
||||
directStreamLocalHandler(srv, conn, newChan, ctx)
|
||||
},
|
||||
"session": ssh.DefaultSessionHandler,
|
||||
},
|
||||
ConnectionFailedCallback: func(conn net.Conn, err error) {
|
||||
s.logger.Warn(ctx, "ssh connection failed",
|
||||
@@ -250,6 +261,12 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
|
||||
// be set before we start listening.
|
||||
HostSigners: []ssh.Signer{},
|
||||
LocalPortForwardingCallback: func(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
|
||||
if s.config.BlockLocalPortForwarding {
|
||||
s.logger.Warn(ctx, "local port forward blocked",
|
||||
slog.F("destination_host", destinationHost),
|
||||
slog.F("destination_port", destinationPort))
|
||||
return false
|
||||
}
|
||||
// Allow local port forwarding all!
|
||||
s.logger.Debug(ctx, "local port forward",
|
||||
slog.F("destination_host", destinationHost),
|
||||
@@ -260,6 +277,12 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
|
||||
return true
|
||||
},
|
||||
ReversePortForwardingCallback: func(ctx ssh.Context, bindHost string, bindPort uint32) bool {
|
||||
if s.config.BlockReversePortForwarding {
|
||||
s.logger.Warn(ctx, "reverse port forward blocked",
|
||||
slog.F("bind_host", bindHost),
|
||||
slog.F("bind_port", bindPort))
|
||||
return false
|
||||
}
|
||||
// Allow reverse port forwarding all!
|
||||
s.logger.Debug(ctx, "reverse port forward",
|
||||
slog.F("bind_host", bindHost),
|
||||
|
||||
@@ -35,8 +35,9 @@ type forwardedStreamLocalPayload struct {
|
||||
// streamlocal forwarding (aka. unix forwarding) instead of TCP forwarding.
|
||||
type forwardedUnixHandler struct {
|
||||
sync.Mutex
|
||||
log slog.Logger
|
||||
forwards map[forwardKey]net.Listener
|
||||
log slog.Logger
|
||||
forwards map[forwardKey]net.Listener
|
||||
blockReversePortForwarding bool
|
||||
}
|
||||
|
||||
type forwardKey struct {
|
||||
@@ -44,10 +45,11 @@ type forwardKey struct {
|
||||
addr string
|
||||
}
|
||||
|
||||
func newForwardedUnixHandler(log slog.Logger) *forwardedUnixHandler {
|
||||
func newForwardedUnixHandler(log slog.Logger, blockReversePortForwarding bool) *forwardedUnixHandler {
|
||||
return &forwardedUnixHandler{
|
||||
log: log,
|
||||
forwards: make(map[forwardKey]net.Listener),
|
||||
log: log,
|
||||
forwards: make(map[forwardKey]net.Listener),
|
||||
blockReversePortForwarding: blockReversePortForwarding,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +64,10 @@ func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server,
|
||||
|
||||
switch req.Type {
|
||||
case "streamlocal-forward@openssh.com":
|
||||
if h.blockReversePortForwarding {
|
||||
log.Warn(ctx, "unix reverse port forward blocked")
|
||||
return false, nil
|
||||
}
|
||||
var reqPayload streamLocalForwardPayload
|
||||
err := gossh.Unmarshal(req.Payload, &reqPayload)
|
||||
if err != nil {
|
||||
|
||||
@@ -211,7 +211,7 @@ func TestServer_X11_EvictionLRU(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
stderr, err := sess.StderrPipe()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sess.Shell())
|
||||
require.NoError(t, sess.Start("sh"))
|
||||
|
||||
// The SSH server lazily starts the session. We need to write a command
|
||||
// and read back to ensure the X11 forwarding is started.
|
||||
|
||||
@@ -32,6 +32,7 @@ func (a *agent) apiHandler() http.Handler {
|
||||
r.Mount("/api/v0/processes", a.processAPI.Routes())
|
||||
r.Mount("/api/v0/desktop", a.desktopAPI.Routes())
|
||||
r.Mount("/api/v0/mcp", a.mcpAPI.Routes())
|
||||
r.Mount("/api/v0/context-config", a.contextConfigAPI.Routes())
|
||||
|
||||
if a.devcontainers {
|
||||
r.Mount("/api/v0/containers", a.containerAPI.Routes())
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package agentdesktop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentssh"
|
||||
@@ -47,6 +52,9 @@ type API struct {
|
||||
logger slog.Logger
|
||||
desktop Desktop
|
||||
clock quartz.Clock
|
||||
|
||||
closeMu sync.Mutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewAPI creates a new desktop streaming API.
|
||||
@@ -66,6 +74,10 @@ func (a *API) Routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/vnc", a.handleDesktopVNC)
|
||||
r.Post("/action", a.handleAction)
|
||||
r.Route("/recording", func(r chi.Router) {
|
||||
r.Post("/start", a.handleRecordingStart)
|
||||
r.Post("/stop", a.handleRecordingStop)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -116,6 +128,9 @@ func (a *API) handleAction(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
handlerStart := a.clock.Now()
|
||||
|
||||
// Update last desktop action timestamp for idle recording monitor.
|
||||
a.desktop.RecordActivity()
|
||||
|
||||
// Ensure the desktop is running and grab native dimensions.
|
||||
cfg, err := a.desktop.Start(ctx)
|
||||
if err != nil {
|
||||
@@ -480,9 +495,150 @@ func (a *API) handleAction(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Close shuts down the desktop session if one is running.
|
||||
func (a *API) Close() error {
|
||||
a.closeMu.Lock()
|
||||
if a.closed {
|
||||
a.closeMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
a.closed = true
|
||||
a.closeMu.Unlock()
|
||||
|
||||
return a.desktop.Close()
|
||||
}
|
||||
|
||||
// decodeRecordingRequest decodes and validates a recording request
|
||||
// from the HTTP body, returning the recording ID. Returns false if
|
||||
// the request was invalid and an error response was already written.
|
||||
func (*API) decodeRecordingRequest(rw http.ResponseWriter, r *http.Request) (string, bool) {
|
||||
ctx := r.Context()
|
||||
var req struct {
|
||||
RecordingID string `json:"recording_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to decode request body.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return "", false
|
||||
}
|
||||
if req.RecordingID == "" {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Missing recording_id.",
|
||||
})
|
||||
return "", false
|
||||
}
|
||||
if _, err := uuid.Parse(req.RecordingID); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid recording_id format.",
|
||||
Detail: "recording_id must be a valid UUID.",
|
||||
})
|
||||
return "", false
|
||||
}
|
||||
return req.RecordingID, true
|
||||
}
|
||||
|
||||
func (a *API) handleRecordingStart(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
recordingID, ok := a.decodeRecordingRequest(rw, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
a.closeMu.Lock()
|
||||
if a.closed {
|
||||
a.closeMu.Unlock()
|
||||
httpapi.Write(ctx, rw, http.StatusServiceUnavailable, codersdk.Response{
|
||||
Message: "Desktop API is shutting down.",
|
||||
})
|
||||
return
|
||||
}
|
||||
a.closeMu.Unlock()
|
||||
|
||||
if err := a.desktop.StartRecording(ctx, recordingID); err != nil {
|
||||
if errors.Is(err, ErrDesktopClosed) {
|
||||
httpapi.Write(ctx, rw, http.StatusServiceUnavailable, codersdk.Response{
|
||||
Message: "Desktop API is shutting down.",
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to start recording.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
|
||||
Message: "Recording started.",
|
||||
})
|
||||
}
|
||||
|
||||
func (a *API) handleRecordingStop(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
recordingID, ok := a.decodeRecordingRequest(rw, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
a.closeMu.Lock()
|
||||
if a.closed {
|
||||
a.closeMu.Unlock()
|
||||
httpapi.Write(ctx, rw, http.StatusServiceUnavailable, codersdk.Response{
|
||||
Message: "Desktop API is shutting down.",
|
||||
})
|
||||
return
|
||||
}
|
||||
a.closeMu.Unlock()
|
||||
|
||||
// Stop recording (idempotent).
|
||||
// Use a context detached from the HTTP request so that if the
|
||||
// connection drops, the recording process can still shut down
|
||||
// gracefully. WithoutCancel preserves request-scoped values.
|
||||
stopCtx, stopCancel := context.WithTimeout(context.WithoutCancel(r.Context()), 30*time.Second)
|
||||
defer stopCancel()
|
||||
artifact, err := a.desktop.StopRecording(stopCtx, recordingID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrUnknownRecording) {
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "Recording not found.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ErrRecordingCorrupted) {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Recording is corrupted.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to stop recording.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
defer artifact.Reader.Close()
|
||||
|
||||
if artifact.Size > workspacesdk.MaxRecordingSize {
|
||||
a.logger.Warn(ctx, "recording file exceeds maximum size",
|
||||
slog.F("recording_id", recordingID),
|
||||
slog.F("size", artifact.Size),
|
||||
slog.F("max_size", workspacesdk.MaxRecordingSize),
|
||||
)
|
||||
httpapi.Write(ctx, rw, http.StatusRequestEntityTooLarge, codersdk.Response{
|
||||
Message: "Recording file exceeds maximum allowed size.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "video/mp4")
|
||||
rw.Header().Set("Content-Length", strconv.FormatInt(artifact.Size, 10))
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, _ = io.Copy(rw, artifact.Reader)
|
||||
}
|
||||
|
||||
// coordFromAction extracts the coordinate pair from a DesktopAction,
|
||||
// returning an error if the coordinate field is missing.
|
||||
func coordFromAction(action DesktopAction) (x, y int, err error) {
|
||||
|
||||
@@ -4,12 +4,17 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/xerrors"
|
||||
@@ -21,6 +26,16 @@ import (
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
// Test recording UUIDs used across tests.
|
||||
const (
|
||||
testRecIDDefault = "870e1f02-8118-4300-a37e-4adb0117baf3"
|
||||
testRecIDStartIdempotent = "250a2ffb-a5e5-4c94-9754-4d6a4ab7ba20"
|
||||
testRecIDStopIdempotent = "38f8a378-f98f-4758-a4ae-950b44cf989a"
|
||||
testRecIDConcurrentA = "8dc173eb-23c6-4601-a485-b6dfb2a42c3a"
|
||||
testRecIDConcurrentB = "fea490d4-70f0-4798-a181-29d65ce25ae1"
|
||||
testRecIDRestart = "75173a0d-b018-4e2e-a771-defa3fc6af69"
|
||||
)
|
||||
|
||||
// Ensure fakeDesktop satisfies the Desktop interface at compile time.
|
||||
var _ agentdesktop.Desktop = (*fakeDesktop)(nil)
|
||||
|
||||
@@ -43,6 +58,14 @@ type fakeDesktop struct {
|
||||
lastTyped string
|
||||
lastKeyDown string
|
||||
lastKeyUp string
|
||||
|
||||
// Recording tracking (guarded by recMu).
|
||||
recMu sync.Mutex
|
||||
recordings map[string]string // ID → file path
|
||||
stopCalls []string // recording IDs passed to StopRecording
|
||||
recStopCh chan string // optional: signaled when StopRecording is called
|
||||
startCount int // incremented on each new recording start
|
||||
activityCount int // incremented by RecordActivity
|
||||
}
|
||||
|
||||
func (f *fakeDesktop) Start(context.Context) (agentdesktop.DisplayConfig, error) {
|
||||
@@ -107,11 +130,140 @@ func (f *fakeDesktop) CursorPosition(context.Context) (x int, y int, err error)
|
||||
return f.cursorPos[0], f.cursorPos[1], nil
|
||||
}
|
||||
|
||||
func (f *fakeDesktop) StartRecording(_ context.Context, recordingID string) error {
|
||||
f.recMu.Lock()
|
||||
defer f.recMu.Unlock()
|
||||
if f.recordings == nil {
|
||||
f.recordings = make(map[string]string)
|
||||
}
|
||||
if path, ok := f.recordings[recordingID]; ok {
|
||||
// Check if already stopped (file still exists but stop was
|
||||
// called). For the fake, a stopped recording means its ID
|
||||
// appears in stopCalls. In that case, remove the old file
|
||||
// and start fresh.
|
||||
stopped := slices.Contains(f.stopCalls, recordingID)
|
||||
if !stopped {
|
||||
// Active recording - no-op.
|
||||
return nil
|
||||
}
|
||||
// Completed recording - discard old file, start fresh.
|
||||
_ = os.Remove(path)
|
||||
delete(f.recordings, recordingID)
|
||||
}
|
||||
f.startCount++
|
||||
tmpFile, err := os.CreateTemp("", "fake-recording-*.mp4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = tmpFile.Write([]byte(fmt.Sprintf("fake-mp4-data-%s-%d", recordingID, f.startCount)))
|
||||
_ = tmpFile.Close()
|
||||
f.recordings[recordingID] = tmpFile.Name()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeDesktop) StopRecording(_ context.Context, recordingID string) (*agentdesktop.RecordingArtifact, error) {
|
||||
f.recMu.Lock()
|
||||
defer f.recMu.Unlock()
|
||||
if f.recordings == nil {
|
||||
return nil, agentdesktop.ErrUnknownRecording
|
||||
}
|
||||
path, ok := f.recordings[recordingID]
|
||||
if !ok {
|
||||
return nil, agentdesktop.ErrUnknownRecording
|
||||
}
|
||||
f.stopCalls = append(f.stopCalls, recordingID)
|
||||
if f.recStopCh != nil {
|
||||
select {
|
||||
case f.recStopCh <- recordingID:
|
||||
default:
|
||||
}
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &agentdesktop.RecordingArtifact{
|
||||
Reader: file,
|
||||
Size: info.Size(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fakeDesktop) RecordActivity() {
|
||||
f.recMu.Lock()
|
||||
f.activityCount++
|
||||
f.recMu.Unlock()
|
||||
}
|
||||
|
||||
func (f *fakeDesktop) Close() error {
|
||||
f.closed = true
|
||||
f.recMu.Lock()
|
||||
defer f.recMu.Unlock()
|
||||
for _, path := range f.recordings {
|
||||
_ = os.Remove(path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// failStartRecordingDesktop wraps fakeDesktop and overrides
|
||||
// StartRecording to always return an error.
|
||||
type failStartRecordingDesktop struct {
|
||||
fakeDesktop
|
||||
startRecordingErr error
|
||||
}
|
||||
|
||||
func (f *failStartRecordingDesktop) StartRecording(_ context.Context, _ string) error {
|
||||
return f.startRecordingErr
|
||||
}
|
||||
|
||||
// corruptedStopDesktop wraps fakeDesktop and overrides
|
||||
// StopRecording to always return ErrRecordingCorrupted.
|
||||
type corruptedStopDesktop struct {
|
||||
fakeDesktop
|
||||
}
|
||||
|
||||
func (*corruptedStopDesktop) StopRecording(_ context.Context, _ string) (*agentdesktop.RecordingArtifact, error) {
|
||||
return nil, agentdesktop.ErrRecordingCorrupted
|
||||
}
|
||||
|
||||
// oversizedFakeDesktop wraps fakeDesktop and expands recording files
|
||||
// beyond MaxRecordingSize when StopRecording is called.
|
||||
type oversizedFakeDesktop struct {
|
||||
fakeDesktop
|
||||
}
|
||||
|
||||
func (f *oversizedFakeDesktop) StopRecording(ctx context.Context, recordingID string) (*agentdesktop.RecordingArtifact, error) {
|
||||
artifact, err := f.fakeDesktop.StopRecording(ctx, recordingID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Close the original reader since we're going to re-open after truncation.
|
||||
artifact.Reader.Close()
|
||||
|
||||
// Look up the path from the fakeDesktop recordings.
|
||||
f.fakeDesktop.recMu.Lock()
|
||||
path := f.fakeDesktop.recordings[recordingID]
|
||||
f.fakeDesktop.recMu.Unlock()
|
||||
|
||||
// Expand the file to exceed the maximum recording size.
|
||||
if err := os.Truncate(path, workspacesdk.MaxRecordingSize+1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Re-open the truncated file.
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &agentdesktop.RecordingArtifact{
|
||||
Reader: file,
|
||||
Size: workspacesdk.MaxRecordingSize + 1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestHandleDesktopVNC_StartError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -134,6 +286,37 @@ func TestHandleDesktopVNC_StartError(t *testing.T) {
|
||||
assert.Equal(t, "Failed to start desktop session.", resp.Message)
|
||||
}
|
||||
|
||||
func TestHandleAction_CallsRecordActivity(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
body := agentdesktop.DesktopAction{
|
||||
Action: "left_click",
|
||||
Coordinate: &[2]int{100, 200},
|
||||
}
|
||||
b, err := json.Marshal(body)
|
||||
require.NoError(t, err)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/action", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler := api.Routes()
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
fake.recMu.Lock()
|
||||
count := fake.activityCount
|
||||
fake.recMu.Unlock()
|
||||
assert.Equal(t, 1, count, "handleAction should call RecordActivity exactly once")
|
||||
}
|
||||
|
||||
func TestHandleAction_Screenshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -574,3 +757,481 @@ func TestHandleAction_CursorPositionReturnsDeclaredCoordinates(t *testing.T) {
|
||||
// Native (960,540) in 1920x1080 should map to declared space in 1280x720.
|
||||
assert.Equal(t, "x=640,y=360", resp.Output)
|
||||
}
|
||||
|
||||
func TestRecordingStartStop(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
// Start recording.
|
||||
startBody, err := json.Marshal(map[string]string{"recording_id": testRecIDDefault})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(startBody))
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
// Stop recording.
|
||||
stopBody, err := json.Marshal(map[string]string{"recording_id": testRecIDDefault})
|
||||
require.NoError(t, err)
|
||||
rr = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(stopBody))
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Equal(t, "video/mp4", rr.Header().Get("Content-Type"))
|
||||
assert.Equal(t, []byte("fake-mp4-data-"+testRecIDDefault+"-1"), rr.Body.Bytes())
|
||||
}
|
||||
|
||||
func TestRecordingStartFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &failStartRecordingDesktop{
|
||||
fakeDesktop: fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
},
|
||||
startRecordingErr: xerrors.New("start recording error"),
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
body, err := json.Marshal(map[string]string{"recording_id": uuid.New().String()})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(body))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
|
||||
var resp codersdk.Response
|
||||
err = json.NewDecoder(rr.Body).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Failed to start recording.", resp.Message)
|
||||
}
|
||||
|
||||
func TestRecordingStartIdempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
// Start same recording twice - both should succeed.
|
||||
for range 2 {
|
||||
body, err := json.Marshal(map[string]string{"recording_id": testRecIDStartIdempotent})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(body))
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
// Stop once, verify normal response.
|
||||
stopBody, err := json.Marshal(map[string]string{"recording_id": testRecIDStartIdempotent})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(stopBody))
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Equal(t, "video/mp4", rr.Header().Get("Content-Type"))
|
||||
assert.Equal(t, []byte("fake-mp4-data-"+testRecIDStartIdempotent+"-1"), rr.Body.Bytes())
|
||||
}
|
||||
|
||||
func TestRecordingStopIdempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
// Start recording.
|
||||
startBody, err := json.Marshal(map[string]string{"recording_id": testRecIDStopIdempotent})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(startBody))
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
// Stop twice - both should succeed with identical data.
|
||||
var bodies [2][]byte
|
||||
for i := range 2 {
|
||||
body, err := json.Marshal(map[string]string{"recording_id": testRecIDStopIdempotent})
|
||||
require.NoError(t, err)
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(body))
|
||||
handler.ServeHTTP(recorder, request)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
assert.Equal(t, "video/mp4", recorder.Header().Get("Content-Type"))
|
||||
bodies[i] = recorder.Body.Bytes()
|
||||
}
|
||||
assert.Equal(t, bodies[0], bodies[1])
|
||||
}
|
||||
|
||||
func TestRecordingStopInvalidIDFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
body, err := json.Marshal(map[string]string{"recording_id": "not-a-uuid"})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(body))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestRecordingStopUnknownRecording(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
// Send a valid UUID that was never started - should reach
|
||||
// StopRecording, get ErrUnknownRecording, and return 404.
|
||||
body, err := json.Marshal(map[string]string{"recording_id": uuid.New().String()})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(body))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusNotFound, rr.Code)
|
||||
|
||||
var resp codersdk.Response
|
||||
err = json.NewDecoder(rr.Body).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Recording not found.", resp.Message)
|
||||
}
|
||||
|
||||
func TestRecordingStopOversizedFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &oversizedFakeDesktop{
|
||||
fakeDesktop: fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
// Start recording.
|
||||
recID := uuid.New().String()
|
||||
startBody, err := json.Marshal(map[string]string{"recording_id": recID})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(startBody))
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
// Stop recording - file exceeds max size, expect 413.
|
||||
stopBody, err := json.Marshal(map[string]string{"recording_id": recID})
|
||||
require.NoError(t, err)
|
||||
rr = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(stopBody))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, rr.Code)
|
||||
|
||||
var resp codersdk.Response
|
||||
err = json.NewDecoder(rr.Body).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Recording file exceeds maximum allowed size.", resp.Message)
|
||||
}
|
||||
|
||||
func TestRecordingMultipleSimultaneous(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
// Start two recordings with different IDs.
|
||||
for _, id := range []string{testRecIDConcurrentA, testRecIDConcurrentB} {
|
||||
body, err := json.Marshal(map[string]string{"recording_id": id})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(body))
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
// Stop both and verify each returns its own data.
|
||||
expected := map[string][]byte{
|
||||
testRecIDConcurrentA: []byte("fake-mp4-data-" + testRecIDConcurrentA + "-1"),
|
||||
testRecIDConcurrentB: []byte("fake-mp4-data-" + testRecIDConcurrentB + "-2"),
|
||||
}
|
||||
for _, id := range []string{testRecIDConcurrentA, testRecIDConcurrentB} {
|
||||
body, err := json.Marshal(map[string]string{"recording_id": id})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(body))
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Equal(t, "video/mp4", rr.Header().Get("Content-Type"))
|
||||
assert.Equal(t, expected[id], rr.Body.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordingStartMalformedBody(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader([]byte("not json")))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestRecordingStartEmptyID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
body, err := json.Marshal(map[string]string{"recording_id": ""})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(body))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestRecordingStopEmptyID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
body, err := json.Marshal(map[string]string{"recording_id": ""})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(body))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestRecordingStopMalformedBody(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader([]byte("not json")))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestRecordingStartAfterCompleted(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
// Step 1: Start recording.
|
||||
startBody, err := json.Marshal(map[string]string{"recording_id": testRecIDRestart})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(startBody))
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
// Step 2: Stop recording (gets first MP4 data).
|
||||
stopBody, err := json.Marshal(map[string]string{"recording_id": testRecIDRestart})
|
||||
require.NoError(t, err)
|
||||
rr = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(stopBody))
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Equal(t, "video/mp4", rr.Header().Get("Content-Type"))
|
||||
firstData := rr.Body.Bytes()
|
||||
require.NotEmpty(t, firstData)
|
||||
|
||||
// Step 3: Start again with the same ID - should succeed
|
||||
// (old file discarded, new recording started).
|
||||
rr = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(startBody))
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
// Step 4: Stop again - should return NEW MP4 data.
|
||||
rr = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(stopBody))
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Equal(t, "video/mp4", rr.Header().Get("Content-Type"))
|
||||
secondData := rr.Body.Bytes()
|
||||
require.NotEmpty(t, secondData)
|
||||
|
||||
// The two recordings should have different data because the
|
||||
// fake increments a counter on each fresh start.
|
||||
assert.NotEqual(t, firstData, secondData,
|
||||
"restarted recording should produce different data")
|
||||
}
|
||||
|
||||
func TestRecordingStartAfterClose(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
// Close the API before sending the request.
|
||||
api.Close()
|
||||
|
||||
body, err := json.Marshal(map[string]string{"recording_id": uuid.New().String()})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(body))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusServiceUnavailable, rr.Code)
|
||||
|
||||
var resp codersdk.Response
|
||||
err = json.NewDecoder(rr.Body).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Desktop API is shutting down.", resp.Message)
|
||||
}
|
||||
|
||||
func TestRecordingStartDesktopClosed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
// StartRecording returns ErrDesktopClosed to simulate a race
|
||||
// where the desktop is closed between the API-level check and
|
||||
// the desktop-level StartRecording call.
|
||||
fake := &failStartRecordingDesktop{
|
||||
fakeDesktop: fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
},
|
||||
startRecordingErr: agentdesktop.ErrDesktopClosed,
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
body, err := json.Marshal(map[string]string{"recording_id": uuid.New().String()})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(body))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusServiceUnavailable, rr.Code)
|
||||
|
||||
var resp codersdk.Response
|
||||
err = json.NewDecoder(rr.Body).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Desktop API is shutting down.", resp.Message)
|
||||
}
|
||||
|
||||
func TestRecordingStopCorrupted(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
fake := &corruptedStopDesktop{
|
||||
fakeDesktop: fakeDesktop{
|
||||
startCfg: agentdesktop.DisplayConfig{Width: 1920, Height: 1080},
|
||||
},
|
||||
}
|
||||
api := agentdesktop.NewAPI(logger, fake, nil)
|
||||
defer api.Close()
|
||||
|
||||
handler := api.Routes()
|
||||
|
||||
// Start a recording so the stop has something to find.
|
||||
recID := uuid.New().String()
|
||||
startBody, err := json.Marshal(map[string]string{"recording_id": recID})
|
||||
require.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/recording/start", bytes.NewReader(startBody))
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
// Stop returns ErrRecordingCorrupted.
|
||||
stopBody, err := json.Marshal(map[string]string{"recording_id": recID})
|
||||
require.NoError(t, err)
|
||||
rr = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodPost, "/recording/stop", bytes.NewReader(stopBody))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
|
||||
var respStop codersdk.Response
|
||||
err = json.NewDecoder(rr.Body).Decode(&respStop)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Recording is corrupted.", respStop.Message)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ package agentdesktop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Desktop abstracts a virtual desktop session running inside a workspace.
|
||||
@@ -58,10 +61,52 @@ type Desktop interface {
|
||||
// CursorPosition returns the current cursor coordinates.
|
||||
CursorPosition(ctx context.Context) (x, y int, err error)
|
||||
|
||||
// RecordActivity marks the desktop as having received user
|
||||
// interaction, resetting the idle-recording timer.
|
||||
RecordActivity()
|
||||
|
||||
// StartRecording begins recording the desktop to an MP4 file
|
||||
// using the caller-provided recording ID. Safe to call
|
||||
// repeatedly - active recordings continue unchanged, stopped
|
||||
// recordings are discarded and restarted. Concurrent recordings
|
||||
// are supported.
|
||||
StartRecording(ctx context.Context, recordingID string) error
|
||||
|
||||
// StopRecording finalizes the recording identified by the given
|
||||
// ID. Idempotent - safe to call on an already-stopped recording.
|
||||
// Returns a RecordingArtifact that the caller can stream. The
|
||||
// caller must close the artifact when done. Returns an error if
|
||||
// the recording ID is unknown.
|
||||
StopRecording(ctx context.Context, recordingID string) (*RecordingArtifact, error)
|
||||
|
||||
// Close shuts down the desktop session and cleans up resources.
|
||||
Close() error
|
||||
}
|
||||
|
||||
// ErrUnknownRecording is returned by StopRecording when the
|
||||
// recording ID is not recognized.
|
||||
var ErrUnknownRecording = xerrors.New("unknown recording ID")
|
||||
|
||||
// ErrDesktopClosed is returned when an operation is attempted on a
|
||||
// closed desktop session.
|
||||
var ErrDesktopClosed = xerrors.New("desktop closed")
|
||||
|
||||
// ErrRecordingCorrupted is returned by StopRecording when the
|
||||
// recording process was force-killed and the artifact is likely
|
||||
// incomplete or corrupt.
|
||||
var ErrRecordingCorrupted = xerrors.New("recording corrupted: process was force-killed")
|
||||
|
||||
// RecordingArtifact is a finalized recording returned by StopRecording.
|
||||
// The caller streams the artifact and must call Close when done. The
|
||||
// artifact remains valid even if the same recording ID is restarted
|
||||
// or the desktop is closed while the caller is reading.
|
||||
type RecordingArtifact struct {
|
||||
// Reader is the MP4 content. Callers must close it when done.
|
||||
Reader io.ReadCloser
|
||||
// Size is the byte length of the MP4 content.
|
||||
Size int64
|
||||
}
|
||||
|
||||
// DisplayConfig describes a running desktop session.
|
||||
type DisplayConfig struct {
|
||||
Width int // native width in pixels
|
||||
|
||||
@@ -3,6 +3,7 @@ package agentdesktop
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
@@ -18,6 +20,7 @@ import (
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
// portableDesktopOutput is the JSON output from
|
||||
@@ -49,32 +52,65 @@ type screenshotOutput struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// recordingProcess tracks a single desktop recording subprocess.
|
||||
type recordingProcess struct {
|
||||
cmd *exec.Cmd
|
||||
filePath string
|
||||
stopped bool
|
||||
killed bool // true when the process was SIGKILLed
|
||||
done chan struct{} // closed when cmd.Wait() returns
|
||||
waitErr error // set before done is closed
|
||||
stopOnce sync.Once
|
||||
idleCancel context.CancelFunc // cancels the per-recording idle goroutine
|
||||
idleDone chan struct{} // closed when idle goroutine exits
|
||||
}
|
||||
|
||||
// maxConcurrentRecordings is the maximum number of active (non-stopped)
|
||||
// recordings allowed at once. This prevents resource exhaustion.
|
||||
const maxConcurrentRecordings = 5
|
||||
|
||||
// idleTimeout is the duration of desktop inactivity after which all
|
||||
// active recordings are automatically stopped.
|
||||
const idleTimeout = 10 * time.Minute
|
||||
|
||||
// portableDesktop implements Desktop by shelling out to the
|
||||
// portabledesktop CLI via agentexec.Execer.
|
||||
type portableDesktop struct {
|
||||
logger slog.Logger
|
||||
execer agentexec.Execer
|
||||
scriptBinDir string // coder script bin directory
|
||||
clock quartz.Clock
|
||||
|
||||
mu sync.Mutex
|
||||
session *desktopSession // nil until started
|
||||
binPath string // resolved path to binary, cached
|
||||
closed bool
|
||||
mu sync.Mutex
|
||||
session *desktopSession // nil until started
|
||||
binPath string // resolved path to binary, cached
|
||||
closed bool
|
||||
recordings map[string]*recordingProcess // guarded by mu
|
||||
lastDesktopActionAt atomic.Int64
|
||||
}
|
||||
|
||||
// NewPortableDesktop creates a Desktop backed by the portabledesktop
|
||||
// CLI binary, using execer to spawn child processes. scriptBinDir is
|
||||
// the coder script bin directory checked for the binary.
|
||||
// the coder script bin directory checked for the binary. If clk is
|
||||
// nil, a real clock is used.
|
||||
func NewPortableDesktop(
|
||||
logger slog.Logger,
|
||||
execer agentexec.Execer,
|
||||
scriptBinDir string,
|
||||
clk quartz.Clock,
|
||||
) Desktop {
|
||||
return &portableDesktop{
|
||||
if clk == nil {
|
||||
clk = quartz.NewReal()
|
||||
}
|
||||
pd := &portableDesktop{
|
||||
logger: logger,
|
||||
execer: execer,
|
||||
scriptBinDir: scriptBinDir,
|
||||
clock: clk,
|
||||
recordings: make(map[string]*recordingProcess),
|
||||
}
|
||||
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
|
||||
return pd
|
||||
}
|
||||
|
||||
// Start launches the desktop session (idempotent).
|
||||
@@ -83,7 +119,7 @@ func (p *portableDesktop) Start(ctx context.Context) (DisplayConfig, error) {
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.closed {
|
||||
return DisplayConfig{}, xerrors.New("desktop is closed")
|
||||
return DisplayConfig{}, ErrDesktopClosed
|
||||
}
|
||||
|
||||
if err := p.ensureBinary(ctx); err != nil {
|
||||
@@ -313,23 +349,328 @@ func (p *portableDesktop) CursorPosition(ctx context.Context) (x int, y int, err
|
||||
return result.X, result.Y, nil
|
||||
}
|
||||
|
||||
// Close shuts down the desktop session and cleans up resources.
|
||||
func (p *portableDesktop) Close() error {
|
||||
// StartRecording begins recording the desktop to an MP4 file.
|
||||
// Three-state idempotency: active recordings are no-ops,
|
||||
// completed recordings are discarded and restarted.
|
||||
func (p *portableDesktop) StartRecording(ctx context.Context, recordingID string) error {
|
||||
// Ensure the desktop session is running before acquiring the
|
||||
// recording lock. Start is independently locked and idempotent.
|
||||
if _, err := p.Start(ctx); err != nil {
|
||||
return xerrors.Errorf("ensure desktop session: %w", err)
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.closed {
|
||||
return ErrDesktopClosed
|
||||
}
|
||||
|
||||
// Three-state idempotency:
|
||||
// - Active recording → no-op, continue recording.
|
||||
// - Completed recording → discard old file, start fresh.
|
||||
// - Unknown ID → fall through to start a new recording.
|
||||
if rec, ok := p.recordings[recordingID]; ok {
|
||||
if !rec.stopped {
|
||||
select {
|
||||
case <-rec.done:
|
||||
// Process exited unexpectedly; treat as completed
|
||||
// so we fall through to discard the old file and
|
||||
// restart.
|
||||
default:
|
||||
// Active recording - no-op, continue recording.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Completed recording - discard old file, start fresh.
|
||||
if err := os.Remove(rec.filePath); err != nil && !os.IsNotExist(err) {
|
||||
p.logger.Warn(ctx, "failed to remove old recording file",
|
||||
slog.F("recording_id", recordingID),
|
||||
slog.F("file_path", rec.filePath),
|
||||
slog.Error(err),
|
||||
)
|
||||
}
|
||||
delete(p.recordings, recordingID)
|
||||
}
|
||||
|
||||
// Check concurrent recording limit.
|
||||
if p.lockedActiveRecordingCount() >= maxConcurrentRecordings {
|
||||
return xerrors.Errorf("too many concurrent recordings (max %d)", maxConcurrentRecordings)
|
||||
}
|
||||
|
||||
// GC sweep: remove stopped recordings with stale files.
|
||||
p.lockedCleanStaleRecordings(ctx)
|
||||
|
||||
if err := p.ensureBinary(ctx); err != nil {
|
||||
return xerrors.Errorf("ensure portabledesktop binary: %w", err)
|
||||
}
|
||||
|
||||
filePath := filepath.Join(os.TempDir(), "coder-recording-"+recordingID+".mp4")
|
||||
|
||||
// Use a background context so the process outlives the HTTP
|
||||
// request that triggered it.
|
||||
procCtx, procCancel := context.WithCancel(context.Background())
|
||||
|
||||
//nolint:gosec // portabledesktop is a trusted binary resolved via ensureBinary.
|
||||
cmd := p.execer.CommandContext(procCtx, p.binPath, "record",
|
||||
// The following options are used to speed up the recording when the desktop is idle.
|
||||
// They were taken out of an example in the portabledesktop repo.
|
||||
// There's likely room for improvement to optimize the values.
|
||||
"--idle-speedup", "20",
|
||||
"--idle-min-duration", "0.35",
|
||||
"--idle-noise-tolerance", "-38dB",
|
||||
filePath)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
procCancel()
|
||||
return xerrors.Errorf("start recording process: %w", err)
|
||||
}
|
||||
|
||||
rec := &recordingProcess{
|
||||
cmd: cmd,
|
||||
filePath: filePath,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go func() {
|
||||
rec.waitErr = cmd.Wait()
|
||||
close(rec.done)
|
||||
// avoid a context resource leak by canceling the context
|
||||
procCancel()
|
||||
}()
|
||||
|
||||
p.recordings[recordingID] = rec
|
||||
|
||||
p.logger.Info(ctx, "started desktop recording",
|
||||
slog.F("recording_id", recordingID),
|
||||
slog.F("file_path", filePath),
|
||||
slog.F("pid", cmd.Process.Pid),
|
||||
)
|
||||
|
||||
// Record activity so a recording started on an already-idle
|
||||
// desktop does not stop immediately.
|
||||
p.lastDesktopActionAt.Store(p.clock.Now().UnixNano())
|
||||
|
||||
// Spawn a per-recording idle goroutine.
|
||||
idleCtx, idleCancel := context.WithCancel(context.Background())
|
||||
rec.idleCancel = idleCancel
|
||||
rec.idleDone = make(chan struct{})
|
||||
go func() {
|
||||
defer close(rec.idleDone)
|
||||
p.monitorRecordingIdle(idleCtx, rec)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopRecording finalizes the recording. Idempotent - safe to call
|
||||
// on an already-stopped recording. Returns a RecordingArtifact
|
||||
// that the caller can stream. The caller must close the Reader
|
||||
// on the returned artifact to avoid leaking file descriptors.
|
||||
func (p *portableDesktop) StopRecording(ctx context.Context, recordingID string) (*RecordingArtifact, error) {
|
||||
p.mu.Lock()
|
||||
rec, ok := p.recordings[recordingID]
|
||||
if !ok {
|
||||
p.mu.Unlock()
|
||||
return nil, ErrUnknownRecording
|
||||
}
|
||||
|
||||
p.lockedStopRecordingProcess(ctx, rec, false)
|
||||
killed := rec.killed
|
||||
p.mu.Unlock()
|
||||
|
||||
p.logger.Info(ctx, "stopped desktop recording",
|
||||
slog.F("recording_id", recordingID),
|
||||
slog.F("file_path", rec.filePath),
|
||||
)
|
||||
|
||||
if killed {
|
||||
return nil, ErrRecordingCorrupted
|
||||
}
|
||||
|
||||
// Open the file and return an artifact. Each call opens a fresh
|
||||
// file descriptor so the caller is insulated from restarts and
|
||||
// desktop close.
|
||||
f, err := os.Open(rec.filePath)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("open recording artifact: %w", err)
|
||||
}
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
return nil, xerrors.Errorf("stat recording artifact: %w", err)
|
||||
}
|
||||
return &RecordingArtifact{
|
||||
Reader: f,
|
||||
Size: info.Size(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// lockedStopRecordingProcess stops a single recording via stopOnce.
|
||||
// It sends SIGINT, waits up to 15 seconds for graceful exit, then
|
||||
// SIGKILLs. When force is true the process is SIGKILLed immediately
|
||||
// without attempting a graceful shutdown. Must be called while p.mu
|
||||
// is held; the lock is held for the full duration so that no
|
||||
// concurrent StopRecording caller can read rec.stopped = true
|
||||
// before the process has finished writing the MP4 file.
|
||||
//
|
||||
//nolint:revive // force flag keeps shared stopOnce/cleanup logic in one place.
|
||||
func (p *portableDesktop) lockedStopRecordingProcess(ctx context.Context, rec *recordingProcess, force bool) {
|
||||
rec.stopOnce.Do(func() {
|
||||
if force {
|
||||
_ = rec.cmd.Process.Kill()
|
||||
rec.killed = true
|
||||
} else {
|
||||
_ = interruptRecordingProcess(rec.cmd.Process)
|
||||
timer := p.clock.NewTimer(15*time.Second, "agentdesktop", "stop_timeout")
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-rec.done:
|
||||
case <-ctx.Done():
|
||||
_ = rec.cmd.Process.Kill()
|
||||
rec.killed = true
|
||||
case <-timer.C:
|
||||
_ = rec.cmd.Process.Kill()
|
||||
rec.killed = true
|
||||
}
|
||||
}
|
||||
rec.stopped = true
|
||||
if rec.idleCancel != nil {
|
||||
rec.idleCancel()
|
||||
}
|
||||
})
|
||||
// NOTE: We intentionally do not wait on rec.done here.
|
||||
// If goleak is added to this package's tests, this may
|
||||
// need revisiting to avoid flakes.
|
||||
}
|
||||
|
||||
// lockedActiveRecordingCount returns the number of recordings that
|
||||
// are still actively running. Must be called while p.mu is held.
|
||||
// The max concurrency is low (maxConcurrentRecordings = 5), so a
|
||||
// full scan is cheap and avoids maintaining a separate counter.
|
||||
func (p *portableDesktop) lockedActiveRecordingCount() int {
|
||||
active := 0
|
||||
for _, rec := range p.recordings {
|
||||
if rec.stopped {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-rec.done:
|
||||
default:
|
||||
active++
|
||||
}
|
||||
}
|
||||
return active
|
||||
}
|
||||
|
||||
// lockedCleanStaleRecordings removes stopped recordings whose temp
|
||||
// files are older than one hour. Must be called while p.mu is held.
|
||||
func (p *portableDesktop) lockedCleanStaleRecordings(ctx context.Context) {
|
||||
for id, rec := range p.recordings {
|
||||
if !rec.stopped {
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(rec.filePath)
|
||||
if err != nil {
|
||||
// File already removed or inaccessible; drop entry.
|
||||
delete(p.recordings, id)
|
||||
continue
|
||||
}
|
||||
if p.clock.Since(info.ModTime()) > time.Hour {
|
||||
if err := os.Remove(rec.filePath); err != nil && !os.IsNotExist(err) {
|
||||
p.logger.Warn(ctx, "failed to remove stale recording file",
|
||||
slog.F("recording_id", id),
|
||||
slog.F("file_path", rec.filePath),
|
||||
slog.Error(err),
|
||||
)
|
||||
}
|
||||
delete(p.recordings, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close shuts down the desktop session and cleans up resources.
|
||||
func (p *portableDesktop) Close() error {
|
||||
p.mu.Lock()
|
||||
p.closed = true
|
||||
if p.session != nil {
|
||||
p.session.cancel()
|
||||
// Xvnc is a child process — killing it cleans up the X
|
||||
// session.
|
||||
_ = p.session.cmd.Process.Kill()
|
||||
_ = p.session.cmd.Wait()
|
||||
p.session = nil
|
||||
|
||||
// Force-kill all active recordings. The stopOnce inside
|
||||
// lockedStopRecordingProcess makes this safe for
|
||||
// already-stopped recordings.
|
||||
for _, rec := range p.recordings {
|
||||
p.lockedStopRecordingProcess(context.Background(), rec, true)
|
||||
}
|
||||
|
||||
// Snapshot recording file paths and idle goroutine channels
|
||||
// for cleanup, then clear the map.
|
||||
type recEntry struct {
|
||||
id string
|
||||
filePath string
|
||||
idleDone chan struct{}
|
||||
}
|
||||
var allRecs []recEntry
|
||||
for id, rec := range p.recordings {
|
||||
allRecs = append(allRecs, recEntry{id: id, filePath: rec.filePath, idleDone: rec.idleDone})
|
||||
delete(p.recordings, id)
|
||||
}
|
||||
session := p.session
|
||||
p.session = nil
|
||||
p.mu.Unlock()
|
||||
|
||||
// Wait for all per-recording idle goroutines to exit.
|
||||
for _, entry := range allRecs {
|
||||
if entry.idleDone != nil {
|
||||
<-entry.idleDone
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all recording files and wait for the session to
|
||||
// exit with a timeout so a slow filesystem or hung process
|
||||
// cannot block agent shutdown indefinitely.
|
||||
cleanupDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(cleanupDone)
|
||||
for _, entry := range allRecs {
|
||||
if err := os.Remove(entry.filePath); err != nil && !os.IsNotExist(err) {
|
||||
p.logger.Warn(context.Background(), "failed to remove recording file on close",
|
||||
slog.F("recording_id", entry.id),
|
||||
slog.F("file_path", entry.filePath),
|
||||
slog.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
if session != nil {
|
||||
session.cancel()
|
||||
if err := session.cmd.Process.Kill(); err != nil {
|
||||
p.logger.Warn(context.Background(), "failed to kill portabledesktop process",
|
||||
slog.Error(err),
|
||||
)
|
||||
}
|
||||
if err := session.cmd.Wait(); err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
p.logger.Warn(context.Background(), "portabledesktop process exited with error",
|
||||
slog.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
timer := p.clock.NewTimer(15*time.Second, "agentdesktop", "close_cleanup_timeout")
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-cleanupDone:
|
||||
case <-timer.C:
|
||||
p.logger.Warn(context.Background(), "timed out waiting for close cleanup")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecordActivity marks the desktop as having received user
|
||||
// interaction, resetting the idle-recording timer.
|
||||
func (p *portableDesktop) RecordActivity() {
|
||||
p.lastDesktopActionAt.Store(p.clock.Now().UnixNano())
|
||||
}
|
||||
|
||||
// runCmd executes a portabledesktop subcommand and returns combined
|
||||
// output. The caller must have previously called ensureBinary.
|
||||
func (p *portableDesktop) runCmd(ctx context.Context, args ...string) (string, error) {
|
||||
@@ -397,3 +738,31 @@ func (p *portableDesktop) ensureBinary(ctx context.Context) error {
|
||||
|
||||
return xerrors.New("portabledesktop binary not found in PATH or script bin directory")
|
||||
}
|
||||
|
||||
// monitorRecordingIdle watches for desktop inactivity and stops the
|
||||
// given recording when the idle timeout is reached.
|
||||
func (p *portableDesktop) monitorRecordingIdle(ctx context.Context, rec *recordingProcess) {
|
||||
timer := p.clock.NewTimer(idleTimeout, "agentdesktop", "recording_idle")
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
lastNano := p.lastDesktopActionAt.Load()
|
||||
lastAction := time.Unix(0, lastNano)
|
||||
elapsed := p.clock.Since(lastAction)
|
||||
if elapsed >= idleTimeout {
|
||||
p.mu.Lock()
|
||||
p.lockedStopRecordingProcess(context.Background(), rec, false)
|
||||
p.mu.Unlock()
|
||||
return
|
||||
}
|
||||
// Activity happened; reset with remaining budget.
|
||||
timer.Reset(idleTimeout-elapsed, "agentdesktop", "recording_idle")
|
||||
case <-rec.done:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,17 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog/v3/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/pty"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
// recordedExecer implements agentexec.Execer by recording every
|
||||
@@ -86,6 +90,7 @@ func TestPortableDesktop_Start_ParsesOutput(t *testing.T) {
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
binPath: "portabledesktop", // pre-set so ensureBinary is a no-op
|
||||
clock: quartz.NewReal(),
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
@@ -117,6 +122,7 @@ func TestPortableDesktop_Start_Idempotent(t *testing.T) {
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
binPath: "portabledesktop",
|
||||
clock: quartz.NewReal(),
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
@@ -159,6 +165,7 @@ func TestPortableDesktop_Screenshot(t *testing.T) {
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
binPath: "portabledesktop",
|
||||
clock: quartz.NewReal(),
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
@@ -184,6 +191,7 @@ func TestPortableDesktop_Screenshot_WithTargetDimensions(t *testing.T) {
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
binPath: "portabledesktop",
|
||||
clock: quartz.NewReal(),
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
@@ -282,6 +290,7 @@ func TestPortableDesktop_MouseMethods(t *testing.T) {
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
binPath: "portabledesktop",
|
||||
clock: quartz.NewReal(),
|
||||
}
|
||||
|
||||
err := tt.invoke(t.Context(), pd)
|
||||
@@ -289,7 +298,6 @@ func TestPortableDesktop_MouseMethods(t *testing.T) {
|
||||
|
||||
cmds := rec.allCommands()
|
||||
require.NotEmpty(t, cmds, "expected at least one command")
|
||||
|
||||
// Find at least one recorded command that contains
|
||||
// all expected argument substrings.
|
||||
found := false
|
||||
@@ -367,6 +375,7 @@ func TestPortableDesktop_KeyboardMethods(t *testing.T) {
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
binPath: "portabledesktop",
|
||||
clock: quartz.NewReal(),
|
||||
}
|
||||
|
||||
err := tt.invoke(t.Context(), pd)
|
||||
@@ -423,6 +432,7 @@ func TestPortableDesktop_Close(t *testing.T) {
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
binPath: "portabledesktop",
|
||||
clock: quartz.NewReal(),
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
@@ -445,7 +455,7 @@ func TestPortableDesktop_Close(t *testing.T) {
|
||||
// Subsequent Start must fail.
|
||||
_, err = pd.Start(ctx)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "desktop is closed")
|
||||
assert.Contains(t, err.Error(), "desktop closed")
|
||||
}
|
||||
|
||||
// --- ensureBinary tests ---
|
||||
@@ -539,7 +549,410 @@ func TestEnsureBinary_NotFound(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
func TestPortableDesktop_StartRecording(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
rec := &recordedExecer{
|
||||
scripts: map[string]string{
|
||||
"record": `trap 'exit 0' INT; sleep 120 & wait`,
|
||||
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
|
||||
},
|
||||
}
|
||||
|
||||
clk := quartz.NewReal()
|
||||
pd := &portableDesktop{
|
||||
logger: logger,
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
clock: clk,
|
||||
binPath: "portabledesktop",
|
||||
recordings: make(map[string]*recordingProcess),
|
||||
}
|
||||
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
|
||||
|
||||
ctx := t.Context()
|
||||
recID := uuid.New().String()
|
||||
err := pd.StartRecording(ctx, recID)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmds := rec.allCommands()
|
||||
require.NotEmpty(t, cmds)
|
||||
// Find the record command (not the up command).
|
||||
found := false
|
||||
for _, cmd := range cmds {
|
||||
joined := strings.Join(cmd, " ")
|
||||
if strings.Contains(joined, "record") && strings.Contains(joined, "coder-recording-"+recID) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "expected a record command with the recording ID")
|
||||
|
||||
require.NoError(t, pd.Close())
|
||||
}
|
||||
|
||||
func TestPortableDesktop_StartRecording_ConcurrentLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
rec := &recordedExecer{
|
||||
scripts: map[string]string{
|
||||
"record": `trap 'exit 0' INT; sleep 120 & wait`,
|
||||
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
|
||||
},
|
||||
}
|
||||
|
||||
clk := quartz.NewReal()
|
||||
pd := &portableDesktop{
|
||||
logger: logger,
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
clock: clk,
|
||||
binPath: "portabledesktop",
|
||||
recordings: make(map[string]*recordingProcess),
|
||||
}
|
||||
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
for i := range maxConcurrentRecordings {
|
||||
err := pd.StartRecording(ctx, uuid.New().String())
|
||||
require.NoError(t, err, "recording %d should succeed", i)
|
||||
}
|
||||
|
||||
err := pd.StartRecording(ctx, uuid.New().String())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "too many concurrent recordings")
|
||||
|
||||
require.NoError(t, pd.Close())
|
||||
}
|
||||
|
||||
func TestPortableDesktop_StopRecording_ReturnsArtifact(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
rec := &recordedExecer{
|
||||
scripts: map[string]string{
|
||||
"record": `trap 'exit 0' INT; sleep 120 & wait`,
|
||||
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
|
||||
},
|
||||
}
|
||||
|
||||
clk := quartz.NewReal()
|
||||
pd := &portableDesktop{
|
||||
logger: logger,
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
clock: clk,
|
||||
binPath: "portabledesktop",
|
||||
recordings: make(map[string]*recordingProcess),
|
||||
}
|
||||
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
|
||||
|
||||
ctx := t.Context()
|
||||
recID := uuid.New().String()
|
||||
err := pd.StartRecording(ctx, recID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Write a dummy MP4 file at the expected path so StopRecording
|
||||
// can open it as an artifact.
|
||||
filePath := filepath.Join(os.TempDir(), "coder-recording-"+recID+".mp4")
|
||||
require.NoError(t, os.WriteFile(filePath, []byte("fake-mp4-data"), 0o600))
|
||||
t.Cleanup(func() { _ = os.Remove(filePath) })
|
||||
|
||||
artifact, err := pd.StopRecording(ctx, recID)
|
||||
require.NoError(t, err)
|
||||
defer artifact.Reader.Close()
|
||||
assert.Equal(t, int64(len("fake-mp4-data")), artifact.Size)
|
||||
|
||||
require.NoError(t, pd.Close())
|
||||
}
|
||||
|
||||
func TestPortableDesktop_StopRecording_UnknownID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
rec := &recordedExecer{
|
||||
scripts: map[string]string{
|
||||
"record": `trap 'exit 0' INT; sleep 120 & wait`,
|
||||
},
|
||||
}
|
||||
|
||||
clk := quartz.NewReal()
|
||||
pd := &portableDesktop{
|
||||
logger: logger,
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
clock: clk,
|
||||
binPath: "portabledesktop",
|
||||
recordings: make(map[string]*recordingProcess),
|
||||
}
|
||||
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
|
||||
|
||||
ctx := t.Context()
|
||||
_, err := pd.StopRecording(ctx, uuid.New().String())
|
||||
require.ErrorIs(t, err, ErrUnknownRecording)
|
||||
|
||||
require.NoError(t, pd.Close())
|
||||
}
|
||||
|
||||
// Ensure that portableDesktop satisfies the Desktop interface at
|
||||
// compile time. This uses the unexported type so it lives in the
|
||||
// internal test package.
|
||||
var _ Desktop = (*portableDesktop)(nil)
|
||||
|
||||
func TestPortableDesktop_IdleTimeout_StopsRecordings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
rec := &recordedExecer{
|
||||
scripts: map[string]string{
|
||||
"record": `trap 'exit 0' INT; sleep 120 & wait`,
|
||||
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
|
||||
},
|
||||
}
|
||||
|
||||
clk := quartz.NewMock(t)
|
||||
pd := &portableDesktop{
|
||||
logger: logger,
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
clock: clk,
|
||||
binPath: "portabledesktop",
|
||||
recordings: make(map[string]*recordingProcess),
|
||||
}
|
||||
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
|
||||
|
||||
ctx := t.Context()
|
||||
recID := uuid.New().String()
|
||||
|
||||
// Install the trap before StartRecording so it is guaranteed
|
||||
// to catch the idle monitor's NewTimer call regardless of
|
||||
// goroutine scheduling.
|
||||
trap := clk.Trap().NewTimer("agentdesktop", "recording_idle")
|
||||
|
||||
err := pd.StartRecording(ctx, recID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify recording is active.
|
||||
pd.mu.Lock()
|
||||
require.False(t, pd.recordings[recID].stopped)
|
||||
pd.mu.Unlock()
|
||||
|
||||
// Wait for the idle monitor timer to be created and release
|
||||
// it so the monitor enters its select loop.
|
||||
trap.MustWait(ctx).MustRelease(ctx)
|
||||
trap.Close()
|
||||
|
||||
// The stop-all path calls lockedStopRecordingProcess which
|
||||
// creates a per-recording 15s stop_timeout timer.
|
||||
stopTrap := clk.Trap().NewTimer("agentdesktop", "stop_timeout")
|
||||
|
||||
// Advance past idle timeout to trigger the stop-all.
|
||||
clk.Advance(idleTimeout)
|
||||
|
||||
// Wait for the stop timer to be created, then release it.
|
||||
stopTrap.MustWait(ctx).MustRelease(ctx)
|
||||
stopTrap.Close()
|
||||
|
||||
// The recording process should now be stopped.
|
||||
require.Eventually(t, func() bool {
|
||||
pd.mu.Lock()
|
||||
defer pd.mu.Unlock()
|
||||
rec, ok := pd.recordings[recID]
|
||||
return ok && rec.stopped
|
||||
}, testutil.WaitShort, testutil.IntervalFast)
|
||||
|
||||
require.NoError(t, pd.Close())
|
||||
}
|
||||
|
||||
func TestPortableDesktop_IdleTimeout_ActivityResetsTimer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
rec := &recordedExecer{
|
||||
scripts: map[string]string{
|
||||
"record": `trap 'exit 0' INT; sleep 120 & wait`,
|
||||
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
|
||||
},
|
||||
}
|
||||
|
||||
clk := quartz.NewMock(t)
|
||||
pd := &portableDesktop{
|
||||
logger: logger,
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
clock: clk,
|
||||
binPath: "portabledesktop",
|
||||
recordings: make(map[string]*recordingProcess),
|
||||
}
|
||||
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
|
||||
|
||||
ctx := t.Context()
|
||||
recID := uuid.New().String()
|
||||
|
||||
// Install the trap before StartRecording so it is guaranteed
|
||||
// to catch the idle monitor's NewTimer call regardless of
|
||||
// goroutine scheduling.
|
||||
trap := clk.Trap().NewTimer("agentdesktop", "recording_idle")
|
||||
|
||||
err := pd.StartRecording(ctx, recID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the idle monitor timer to be created.
|
||||
trap.MustWait(ctx).MustRelease(ctx)
|
||||
trap.Close()
|
||||
|
||||
// Advance most of the way but not past the timeout.
|
||||
clk.Advance(idleTimeout - time.Minute)
|
||||
|
||||
// Record activity to reset the timer.
|
||||
pd.RecordActivity()
|
||||
|
||||
// Trap the Reset call that the idle monitor makes when it
|
||||
// sees recent activity.
|
||||
resetTrap := clk.Trap().TimerReset("agentdesktop", "recording_idle")
|
||||
|
||||
// Advance past the original idle timeout deadline. The
|
||||
// monitor should see the recent activity and reset instead
|
||||
// of stopping.
|
||||
clk.Advance(time.Minute)
|
||||
|
||||
resetTrap.MustWait(ctx).MustRelease(ctx)
|
||||
resetTrap.Close()
|
||||
|
||||
// Recording should still be active because activity was
|
||||
// recorded.
|
||||
pd.mu.Lock()
|
||||
require.False(t, pd.recordings[recID].stopped)
|
||||
pd.mu.Unlock()
|
||||
|
||||
require.NoError(t, pd.Close())
|
||||
}
|
||||
|
||||
func TestPortableDesktop_IdleTimeout_MultipleRecordings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
rec := &recordedExecer{
|
||||
scripts: map[string]string{
|
||||
"record": `trap 'exit 0' INT; sleep 120 & wait`,
|
||||
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
|
||||
},
|
||||
}
|
||||
|
||||
clk := quartz.NewMock(t)
|
||||
pd := &portableDesktop{
|
||||
logger: logger,
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
clock: clk,
|
||||
binPath: "portabledesktop",
|
||||
recordings: make(map[string]*recordingProcess),
|
||||
}
|
||||
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
|
||||
|
||||
ctx := t.Context()
|
||||
recID1 := uuid.New().String()
|
||||
recID2 := uuid.New().String()
|
||||
|
||||
// Trap idle timer creation for both recordings.
|
||||
trap := clk.Trap().NewTimer("agentdesktop", "recording_idle")
|
||||
|
||||
err := pd.StartRecording(ctx, recID1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for first recording's idle timer.
|
||||
trap.MustWait(ctx).MustRelease(ctx)
|
||||
|
||||
err = pd.StartRecording(ctx, recID2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for second recording's idle timer.
|
||||
trap.MustWait(ctx).MustRelease(ctx)
|
||||
trap.Close()
|
||||
|
||||
// Trap the stop timers that will be created when idle fires.
|
||||
stopTrap := clk.Trap().NewTimer("agentdesktop", "stop_timeout")
|
||||
|
||||
// Advance past idle timeout.
|
||||
clk.Advance(idleTimeout)
|
||||
|
||||
// Wait for both stop timers.
|
||||
stopTrap.MustWait(ctx).MustRelease(ctx)
|
||||
stopTrap.MustWait(ctx).MustRelease(ctx)
|
||||
stopTrap.Close()
|
||||
|
||||
// Both recordings should be stopped.
|
||||
require.Eventually(t, func() bool {
|
||||
pd.mu.Lock()
|
||||
defer pd.mu.Unlock()
|
||||
r1, ok1 := pd.recordings[recID1]
|
||||
r2, ok2 := pd.recordings[recID2]
|
||||
return ok1 && r1.stopped && ok2 && r2.stopped
|
||||
}, testutil.WaitShort, testutil.IntervalFast)
|
||||
|
||||
require.NoError(t, pd.Close())
|
||||
}
|
||||
|
||||
func TestPortableDesktop_StartRecording_ReturnsErrDesktopClosed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
rec := &recordedExecer{
|
||||
scripts: map[string]string{
|
||||
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
|
||||
},
|
||||
}
|
||||
|
||||
clk := quartz.NewReal()
|
||||
pd := &portableDesktop{
|
||||
logger: logger,
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
clock: clk,
|
||||
binPath: "portabledesktop",
|
||||
recordings: make(map[string]*recordingProcess),
|
||||
}
|
||||
pd.lastDesktopActionAt.Store(clk.Now().UnixNano())
|
||||
|
||||
// Start and close the desktop so it's in the closed state.
|
||||
ctx := t.Context()
|
||||
_, err := pd.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, pd.Close())
|
||||
|
||||
// StartRecording should now return ErrDesktopClosed.
|
||||
err = pd.StartRecording(ctx, uuid.New().String())
|
||||
require.ErrorIs(t, err, ErrDesktopClosed)
|
||||
}
|
||||
|
||||
func TestPortableDesktop_Start_ReturnsErrDesktopClosed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
rec := &recordedExecer{
|
||||
scripts: map[string]string{
|
||||
"up": `printf '{"vncPort":5901,"geometry":"1920x1080"}\n' && sleep 120`,
|
||||
},
|
||||
}
|
||||
|
||||
pd := &portableDesktop{
|
||||
logger: logger,
|
||||
execer: rec,
|
||||
scriptBinDir: t.TempDir(),
|
||||
clock: quartz.NewReal(),
|
||||
binPath: "portabledesktop",
|
||||
recordings: make(map[string]*recordingProcess),
|
||||
}
|
||||
pd.lastDesktopActionAt.Store(pd.clock.Now().UnixNano())
|
||||
|
||||
ctx := t.Context()
|
||||
_, err := pd.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, pd.Close())
|
||||
|
||||
_, err = pd.Start(ctx)
|
||||
require.ErrorIs(t, err, ErrDesktopClosed)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
//go:build !windows
|
||||
|
||||
package agentdesktop
|
||||
|
||||
import "os"
|
||||
|
||||
// interruptRecordingProcess sends a SIGINT to the recording process
|
||||
// for graceful shutdown. On Unix, os.Interrupt is delivered as
|
||||
// SIGINT which lets the recorder finalize the MP4 container.
|
||||
func interruptRecordingProcess(p *os.Process) error {
|
||||
return p.Signal(os.Interrupt)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package agentdesktop
|
||||
|
||||
import "os"
|
||||
|
||||
// interruptRecordingProcess kills the recording process directly
|
||||
// because os.Process.Signal(os.Interrupt) is not supported on
|
||||
// Windows and returns an error without delivering a signal.
|
||||
func interruptRecordingProcess(p *os.Process) error {
|
||||
return p.Kill()
|
||||
}
|
||||
+39
-12
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -70,16 +69,40 @@ func NewManager(logger slog.Logger) *Manager {
|
||||
}
|
||||
}
|
||||
|
||||
// Connect discovers .mcp.json in dir and connects to all
|
||||
// configured servers. Failed servers are logged and skipped.
|
||||
func (m *Manager) Connect(ctx context.Context, dir string) error {
|
||||
path := filepath.Join(dir, ".mcp.json")
|
||||
configs, err := ParseConfig(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
// Connect reads MCP config files at the given absolute paths and
|
||||
// connects to all configured servers. Failed servers are logged
|
||||
// and skipped. Missing config files are silently skipped.
|
||||
func (m *Manager) Connect(ctx context.Context, mcpConfigFiles []string) error {
|
||||
var allConfigs []ServerConfig
|
||||
for _, configPath := range mcpConfigFiles {
|
||||
configs, err := ParseConfig(configPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
m.logger.Warn(ctx, "failed to parse MCP config",
|
||||
slog.F("path", configPath),
|
||||
slog.Error(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
return xerrors.Errorf("parse mcp config: %w", err)
|
||||
allConfigs = append(allConfigs, configs...)
|
||||
}
|
||||
|
||||
// Deduplicate by server name; first occurrence wins.
|
||||
seen := make(map[string]struct{})
|
||||
deduped := make([]ServerConfig, 0, len(allConfigs))
|
||||
for _, cfg := range allConfigs {
|
||||
if _, ok := seen[cfg.Name]; ok {
|
||||
continue
|
||||
}
|
||||
seen[cfg.Name] = struct{}{}
|
||||
deduped = append(deduped, cfg)
|
||||
}
|
||||
allConfigs = deduped
|
||||
|
||||
if len(allConfigs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connect to servers in parallel without holding the
|
||||
@@ -95,7 +118,7 @@ func (m *Manager) Connect(ctx context.Context, dir string) error {
|
||||
connected []connectedServer
|
||||
)
|
||||
var eg errgroup.Group
|
||||
for _, cfg := range configs {
|
||||
for _, cfg := range allConfigs {
|
||||
eg.Go(func() error {
|
||||
c, err := m.connectServer(ctx, cfg)
|
||||
if err != nil {
|
||||
@@ -164,7 +187,11 @@ func (*Manager) connectServer(ctx context.Context, cfg ServerConfig) (*client.Cl
|
||||
connectCtx, cancel := context.WithTimeout(ctx, connectTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := c.Start(connectCtx); err != nil {
|
||||
// Use the parent ctx (not connectCtx) so the subprocess outlives
|
||||
// the connect/initialize handshake. connectCtx bounds only the
|
||||
// Initialize call below. The subprocess is cleaned up when the
|
||||
// Manager is closed or ctx is canceled.
|
||||
if err := c.Start(ctx); err != nil {
|
||||
_ = c.Close()
|
||||
return nil, xerrors.Errorf("start %q: %w", cfg.Name, err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package agentmcp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
@@ -8,6 +13,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestSplitToolName(t *testing.T) {
|
||||
@@ -193,3 +199,118 @@ func TestConvertResult(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnectServer_StdioProcessSurvivesConnect verifies that a stdio MCP
|
||||
// server subprocess remains alive after connectServer returns. This is a
|
||||
// regression test for a bug where the subprocess was tied to a short-lived
|
||||
// connectCtx and killed as soon as the context was canceled.
|
||||
func TestConnectServer_StdioProcessSurvivesConnect(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if os.Getenv("TEST_MCP_FAKE_SERVER") == "1" {
|
||||
// Child process: act as a minimal MCP server over stdio.
|
||||
runFakeMCPServer()
|
||||
return
|
||||
}
|
||||
|
||||
// Get the path to the test binary so we can re-exec ourselves
|
||||
// as a fake MCP server subprocess.
|
||||
testBin, err := os.Executable()
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := ServerConfig{
|
||||
Name: "fake",
|
||||
Transport: "stdio",
|
||||
Command: testBin,
|
||||
Args: []string{"-test.run=^TestConnectServer_StdioProcessSurvivesConnect$"},
|
||||
Env: map[string]string{"TEST_MCP_FAKE_SERVER": "1"},
|
||||
}
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
m := &Manager{}
|
||||
client, err := m.connectServer(ctx, cfg)
|
||||
require.NoError(t, err, "connectServer should succeed")
|
||||
t.Cleanup(func() { _ = client.Close() })
|
||||
|
||||
// At this point connectServer has returned and its internal
|
||||
// connectCtx has been canceled. The subprocess must still be
|
||||
// alive. Verify by listing tools (requires a live server).
|
||||
listCtx, listCancel := context.WithTimeout(ctx, testutil.WaitShort)
|
||||
defer listCancel()
|
||||
result, err := client.ListTools(listCtx, mcp.ListToolsRequest{})
|
||||
require.NoError(t, err, "ListTools should succeed — server must be alive after connect")
|
||||
require.Len(t, result.Tools, 1)
|
||||
assert.Equal(t, "echo", result.Tools[0].Name)
|
||||
}
|
||||
|
||||
// runFakeMCPServer implements a minimal JSON-RPC / MCP server over
|
||||
// stdin/stdout, just enough for initialize + tools/list.
|
||||
func runFakeMCPServer() {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
|
||||
var req struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.RawMessage `json:"id"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
if err := json.Unmarshal(line, &req); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var resp any
|
||||
switch req.Method {
|
||||
case "initialize":
|
||||
resp = map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": req.ID,
|
||||
"result": map[string]any{
|
||||
"protocolVersion": "2025-03-26",
|
||||
"capabilities": map[string]any{
|
||||
"tools": map[string]any{},
|
||||
},
|
||||
"serverInfo": map[string]any{
|
||||
"name": "fake-server",
|
||||
"version": "0.0.1",
|
||||
},
|
||||
},
|
||||
}
|
||||
case "notifications/initialized":
|
||||
// No response needed for notifications.
|
||||
continue
|
||||
case "tools/list":
|
||||
resp = map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": req.ID,
|
||||
"result": map[string]any{
|
||||
"tools": []map[string]any{
|
||||
{
|
||||
"name": "echo",
|
||||
"description": "echoes input",
|
||||
"inputSchema": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
default:
|
||||
resp = map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": req.ID,
|
||||
"error": map[string]any{
|
||||
"code": -32601,
|
||||
"message": "method not found",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, _ = fmt.Fprintf(os.Stdout, "%s\n", out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type Copilot struct {
|
||||
// Name is the provider instance name. If empty, defaults to "copilot".
|
||||
Name string
|
||||
BaseURL string
|
||||
APIDumpDir string
|
||||
CircuitBreaker *CircuitBreaker
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"}}
|
||||
|
||||
@@ -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
+146
@@ -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}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user