mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-08 01:46:23 +00:00
Compare commits
45
Commits
desktop-ssh
..
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11e2e0a59f | ||
|
|
d1d1c6f890 | ||
|
|
0ab661a9cc | ||
|
|
a55dc8c84a | ||
|
|
be41bc4e7d | ||
|
|
567f8b9743 | ||
|
|
6263a35b3f | ||
|
|
74ca560c75 | ||
|
|
a68d6f904d | ||
|
|
cc8c2f8810 | ||
|
|
ad31bff969 | ||
|
|
a5312e169b | ||
|
|
16aca14bc7 | ||
|
|
4aba093c98 | ||
|
|
a3bbcd5c73 | ||
|
|
c05d07cd73 | ||
|
|
ef34ada9fb | ||
|
|
e15fb426ec | ||
|
|
72433f2ed8 | ||
|
|
b32d8c3e58 | ||
|
|
6af8515f69 | ||
|
|
5c50edb9bb | ||
|
|
fcddc84225 | ||
|
|
582a2108ce | ||
|
|
9c65a69937 | ||
|
|
1d391908f4 | ||
|
|
596dca4dee | ||
|
|
1827832775 | ||
|
|
898692af26 | ||
|
|
5c3f2ddf8c | ||
|
|
1382cebe10 | ||
|
|
0732cdd8e1 | ||
|
|
2bf6a9813a | ||
|
|
f094a10645 | ||
|
|
89f1943d2d | ||
|
|
fc4f319676 | ||
|
|
b51fdb331c | ||
|
|
8a1a6b8fb4 | ||
|
|
1a2b985777 | ||
|
|
00067d23a0 | ||
|
|
3ebc4b01b9 | ||
|
|
17f362e0fe | ||
|
|
5b83c15fb5 | ||
|
|
d86c3041c9 | ||
|
|
3cf197a309 |
@@ -1,5 +1,5 @@
|
||||
---
|
||||
"@opencode-ai/core": patch
|
||||
"@opencode/core": patch
|
||||
---
|
||||
|
||||
Correct directory page headings when the read offset is zero.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: deploy-files
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- v2
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-files-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'v2')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Typecheck
|
||||
working-directory: services/files
|
||||
run: bun typecheck
|
||||
|
||||
- name: Deploy
|
||||
working-directory: services/files
|
||||
run: bun run deploy --env ${{ github.ref_name == 'v2' && 'production' || 'dev' }}
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -24,13 +24,13 @@ jobs:
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Build
|
||||
working-directory: packages/www
|
||||
working-directory: services/www
|
||||
run: bun run build
|
||||
env:
|
||||
CLOUDFLARE_ENV: ${{ github.ref_name == 'v2' && 'production' || 'dev' }}
|
||||
|
||||
- name: Deploy
|
||||
working-directory: packages/www
|
||||
working-directory: services/www
|
||||
run: bun run deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
@@ -11,6 +11,7 @@ on:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
- "services/*/package.json"
|
||||
- "flake.lock"
|
||||
- "nix/node_modules.nix"
|
||||
- "nix/scripts/**"
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
- name: Deploy update service
|
||||
if: github.ref_name == 'v2'
|
||||
working-directory: packages/updates
|
||||
working-directory: services/updates
|
||||
run: bun run deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -670,19 +670,6 @@ jobs:
|
||||
git config --global user.name "opencode"
|
||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
||||
|
||||
- name: Upload desktop release assets
|
||||
if: needs.version.outputs.release
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
files=(/tmp/desktop/*.{exe,blockmap,dmg,zip,AppImage,deb,rpm} /tmp/desktop/*.app.tar.gz)
|
||||
if (( ${#files[@]} == 0 )); then
|
||||
echo "No desktop release assets found"
|
||||
exit 1
|
||||
fi
|
||||
gh release upload "v${{ needs.version.outputs.version }}" "${files[@]}" --clobber --repo "${{ needs.version.outputs.repo }}"
|
||||
|
||||
- run: ./script/publish.ts
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
@@ -694,3 +681,6 @@ jobs:
|
||||
LATEST_YML_DIR: /tmp/latest-yml
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
OPENCODE_DESKTOP_DIST: /tmp/desktop
|
||||
CLOUDFLARE_ACCOUNT_ID: 15d29c8639fd3733b1b5486a2acfd968
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
echo "app=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
bun x turbo@2.10.2 ls --affected --filter=@opencode-ai/app --output=json > affected.json
|
||||
bun x turbo@2.10.2 ls --affected --filter=@opencode/app --output=json > affected.json
|
||||
bun -e 'const result = await Bun.file("affected.json").json(); console.log(`app=${result.packages.count > 0}`)' >> "$GITHUB_OUTPUT"
|
||||
|
||||
unit:
|
||||
@@ -132,10 +132,10 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
bun turbo verify:package --filter=@opencode-ai/sdk
|
||||
bun turbo verify:package --filter=@opencode/sdk
|
||||
exit 0
|
||||
fi
|
||||
bun turbo verify:package --affected --filter=@opencode-ai/sdk
|
||||
bun turbo verify:package --affected --filter=@opencode/sdk
|
||||
env:
|
||||
TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }}
|
||||
TURBO_SCM_HEAD: ${{ github.sha }}
|
||||
@@ -173,7 +173,7 @@ jobs:
|
||||
|
||||
- name: Check generated documentation
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: packages/www
|
||||
working-directory: services/www
|
||||
run: bun run check:generated
|
||||
|
||||
e2e:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import { tool } from "@opencode/plugin"
|
||||
async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
||||
...options,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import { tool } from "@opencode/plugin"
|
||||
|
||||
const TEAM = {
|
||||
tui: ["kommander", "simonklee"],
|
||||
|
||||
@@ -84,9 +84,9 @@ const { a, b } = obj
|
||||
### Imports
|
||||
|
||||
- Never alias imports. Do not use `import { foo as bar } from "..."` or renamed imports like `resolve as pathResolve`.
|
||||
- Never use type-position `import("...")` references such as `Schema.declare<import("@opencode-ai/plugin/effect/plugin").Plugin["effect"]>`. Only when two imports genuinely collide on a name and no other option exists, an aliased type import (`import type { Plugin as PluginDefinition } from "..."`) is permitted as a last resort — still strongly preferred not to.
|
||||
- Never use type-position `import("...")` references such as `Schema.declare<import("@opencode/plugin/effect/plugin").Plugin["effect"]>`. Only when two imports genuinely collide on a name and no other option exists, an aliased type import (`import type { Plugin as PluginDefinition } from "..."`) is permitted as a last resort — still strongly preferred not to.
|
||||
- Never use star imports. Do not use `import * as Foo from "..."` or `import type * as Foo from "..."`.
|
||||
- If a namespace-style value is needed, import the module's own exported namespace by name, for example `import { Project } from "@opencode-ai/core/project"`, then reference `Project.ID`.
|
||||
- If a namespace-style value is needed, import the module's own exported namespace by name, for example `import { Project } from "@opencode/core/project"`, then reference `Project.ID`.
|
||||
- Prefer dynamic imports for heavy modules that are only needed in selected code paths, especially in startup-sensitive entrypoints. Destructure dynamic import bindings near the top of the narrowest scope that needs them so they read like normal imports. Avoid inline chains such as `await import("./module").then((mod) => mod.value())` or `(await import("./module")).value()`. Keep branch-specific imports inside the branch that needs them to preserve lazy loading.
|
||||
|
||||
### Variables
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
exact = true
|
||||
# Only install newly resolved package versions published at least 3 days ago.
|
||||
minimumReleaseAge = 259200
|
||||
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@brendonovich/vite-plugin-opencode", "@opencode-ai/sdk", "@opencode-ai/pty", "@opencode-ai/pty-darwin-arm64", "@opencode-ai/pty-darwin-x64", "@opencode-ai/pty-linux-arm64-gnu", "@opencode-ai/pty-linux-arm64-musl", "@opencode-ai/pty-linux-x64-gnu", "@opencode-ai/pty-linux-x64-musl", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron", "electron-builder", "electron-publish", "blume", "mermaid"]
|
||||
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@brendonovich/vite-plugin-opencode", "@opencode/sdk", "@opencode-ai/pty", "@opencode-ai/pty-darwin-arm64", "@opencode-ai/pty-darwin-x64", "@opencode-ai/pty-linux-arm64-gnu", "@opencode-ai/pty-linux-arm64-musl", "@opencode-ai/pty-linux-x64-gnu", "@opencode-ai/pty-linux-x64-musl", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron", "electron-builder", "electron-publish", "blume", "mermaid"]
|
||||
|
||||
[test]
|
||||
root = "./do-not-run-tests-from-root"
|
||||
|
||||
@@ -165,22 +165,30 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
package_scope="@opencode"
|
||||
if [ -z "$requested_version" ]; then
|
||||
metadata=$(curl -fsSL https://registry.npmjs.org/@opencode-ai%2fcli/beta || true)
|
||||
metadata=$(curl -fsSL https://opencode.ai/update/api/beta/cli/npm || true)
|
||||
specific_version=$(echo "$metadata" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
|
||||
package=$(echo "$metadata" | sed -n 's/.*"package":"\([^"]*\)".*/\1/p')
|
||||
|
||||
if [ -z "$specific_version" ]; then
|
||||
if [ -z "$specific_version" ] || [ -z "$package" ]; then
|
||||
echo -e "${RED}Failed to fetch version information${NC}"
|
||||
exit 1
|
||||
fi
|
||||
package_scope="${package%/cli}"
|
||||
else
|
||||
# Strip leading 'v' if present
|
||||
requested_version="${requested_version#v}"
|
||||
specific_version=$requested_version
|
||||
fi
|
||||
|
||||
package_name="@opencode-ai/cli-$target"
|
||||
http_status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/@opencode-ai%2fcli-$target/$specific_version" || true)
|
||||
package_name="$package_scope/cli-$target"
|
||||
http_status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/$package_scope%2fcli-$target/$specific_version" || true)
|
||||
# Older clients install the minimum release before they can migrate package names.
|
||||
if [ "$http_status" = "404" ] && [ -n "$requested_version" ]; then
|
||||
package_name="@opencode-ai/cli-$target"
|
||||
http_status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/@opencode-ai%2fcli-$target/$specific_version" || true)
|
||||
fi
|
||||
if [ "$http_status" = "404" ]; then
|
||||
echo -e "${RED}Error: Version ${specific_version} is not available for $target${NC}"
|
||||
echo -e "${MUTED}Available versions: https://www.npmjs.com/package/$package_name?activeTab=versions${NC}"
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
cd packages/desktop
|
||||
|
||||
export OPENCODE_CLI_DIST="$TMPDIR/desktop-cli"
|
||||
cli_package=$(bun -e 'import { getCurrentCli } from "./scripts/utils.ts"; console.log(getCurrentCli().package.replace("@opencode-ai/", ""))')
|
||||
cli_package=$(bun -e 'import { getCurrentCli } from "./scripts/utils.ts"; console.log(getCurrentCli().package.replace("@opencode/", ""))')
|
||||
mkdir -p "$OPENCODE_CLI_DIST/$cli_package/bin"
|
||||
cp ${lib.getExe opencode} "$OPENCODE_CLI_DIST/$cli_package/bin/opencode2"
|
||||
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-T1JUv8gUrXavDI1HdVGqNRal4y2Bh5QavpR4DV+bJdA=",
|
||||
"aarch64-linux": "sha256-wlbWdEUVWAafgxaZis0Q+SAas8k6hu3s0C39CnMZ2gw=",
|
||||
"aarch64-darwin": "sha256-74rRue8KQWKzgMMb540beqMzp1DUShlPte6qklYCVTs=",
|
||||
"x86_64-darwin": "sha256-ihr3DoLd/4Lw6WbyXzSQXMP4hrVGk/UiRkO2aoQn8vg="
|
||||
"x86_64-linux": "sha256-gW+1JbPQnt+PZzHfGijjnwch6X6pyaBnPTxAc47yiQw=",
|
||||
"aarch64-linux": "sha256-qI6CtJkmh20ouWeiQRR1zp+YjHootkN23VFJCwqnf0A=",
|
||||
"aarch64-darwin": "sha256-oyEhVcjbKq3+BCCxVBl0TZYmKJv1BBLWPVs0Vw6Fbac=",
|
||||
"x86_64-darwin": "sha256-UhLZjG3NlkoXRWNn8SK/pLm5eDR3saU+TgEgfX592vQ="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,12 @@ stdenvNoCC.mkDerivation {
|
||||
fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
|
||||
lib.fileset.unions [
|
||||
../packages
|
||||
../services
|
||||
../bun.lock
|
||||
../package.json
|
||||
../patches
|
||||
../install # required by desktop build (cli.rs include_str!)
|
||||
../.github/TEAM_MEMBERS # required by @opencode-ai/script
|
||||
../.github/TEAM_MEMBERS # required by @opencode/script
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
+4
-3
@@ -13,7 +13,7 @@
|
||||
"dev:web": "bun --cwd packages/app dev",
|
||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
||||
"dev:www": "bun run --cwd packages/www dev",
|
||||
"dev:www": "bun run --cwd services/www dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"bench:devex": "bun run --cwd packages/app test:bench:devex",
|
||||
"lint": "oxlint",
|
||||
@@ -34,6 +34,7 @@
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"services/*",
|
||||
"packages/console/*",
|
||||
"packages/stats/*"
|
||||
],
|
||||
@@ -128,8 +129,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opencode/script": "workspace:*",
|
||||
"heap-snapshot-toolkit": "1.1.3",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
|
||||
@@ -121,10 +121,10 @@ Keep provider facades small and explicit:
|
||||
|
||||
### Provider Package Entrypoints
|
||||
|
||||
Catalog-selected native providers use package-like export paths from `@opencode-ai/ai`. They are internal entrypoints in one npm package, not separately published provider packages. Every entrypoint implements `ProviderPackage.Definition` and exposes `model(modelID, settings)`, where settings are serializable provider configuration plus common `headers`, `body`, and `limits` overlays.
|
||||
Catalog-selected native providers use package-like export paths from `@opencode/ai`. They are internal entrypoints in one npm package, not separately published provider packages. Every entrypoint implements `ProviderPackage.Definition` and exposes `model(modelID, settings)`, where settings are serializable provider configuration plus common `headers`, `body`, and `limits` overlays.
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/openai/responses"
|
||||
import { model } from "@opencode/ai/providers/openai/responses"
|
||||
|
||||
const selected = model("gpt-5", {
|
||||
apiKey,
|
||||
|
||||
+153
-49
@@ -1,12 +1,12 @@
|
||||
# @opencode-ai/ai
|
||||
# @opencode/ai
|
||||
|
||||
Schema-first AI primitives for opencode. Provider quirks live in adapters, not in calling code.
|
||||
Schema-first language model and image-generation APIs built with Effect.
|
||||
|
||||
```ts
|
||||
import { Effect, Layer } from "effect"
|
||||
import { LLM, LLMClient } from "@opencode-ai/ai"
|
||||
import { RequestExecutor } from "@opencode-ai/ai/route"
|
||||
import { OpenAI } from "@opencode-ai/ai/providers"
|
||||
import { LLM, LLMClient } from "@opencode/ai"
|
||||
import { RequestExecutor } from "@opencode/ai/route"
|
||||
import { OpenAI } from "@opencode/ai/providers"
|
||||
|
||||
const model = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses("gpt-4o-mini")
|
||||
|
||||
@@ -29,13 +29,123 @@ await Effect.runPromise(program.pipe(Effect.provide(llmLayer)))
|
||||
|
||||
Run `LLMClient.stream(request)` instead of `generate` when you want incremental `LLMEvent`s. The event stream is provider-neutral — same shape across OpenAI Chat, OpenAI Responses, Anthropic Messages, Gemini, Bedrock Converse, and any OpenAI-compatible deployment.
|
||||
|
||||
## MiniMax
|
||||
|
||||
MiniMax defaults to its Messages API and reads `MINIMAX_API_KEY` when `apiKey` is omitted:
|
||||
|
||||
```ts
|
||||
import { Effect, Layer } from "effect"
|
||||
import { LLM, LLMClient } from "@opencode/ai"
|
||||
import { MiniMax } from "@opencode/ai/providers"
|
||||
import { RequestExecutor } from "@opencode/ai/route"
|
||||
|
||||
const minimax = MiniMax.configure({ apiKey: process.env.MINIMAX_API_KEY })
|
||||
const request = LLM.request({
|
||||
model: minimax.model("MiniMax-M3"), // also minimax.messages("MiniMax-M3")
|
||||
prompt: "What is 173 multiplied by 219?",
|
||||
providerOptions: { thinking: { type: "adaptive" } },
|
||||
generation: { maxTokens: 1536 },
|
||||
})
|
||||
|
||||
const layer = LLMClient.layer.pipe(Layer.provide(RequestExecutor.fetchLayer))
|
||||
const response = await Effect.runPromise(LLMClient.generate(request).pipe(Effect.provide(layer)))
|
||||
console.log(response.text)
|
||||
```
|
||||
|
||||
Select `minimax.chat("MiniMax-M3")` or `minimax.responses("MiniMax-M3")` for MiniMax's native Chat Completions
|
||||
and Responses APIs. The matching package entrypoints are `@opencode/ai/providers/minimax/messages`,
|
||||
`@opencode/ai/providers/minimax/chat`, and `@opencode/ai/providers/minimax/responses`.
|
||||
|
||||
- **Messages:** M3 thinking defaults off. Set `thinking: { type: "adaptive" }` to enable it or
|
||||
`thinking: { type: "disabled" }` to disable it.
|
||||
- **Chat:** M3 thinking defaults on and uses the same `thinking` control. The provider enables `reasoning_split`
|
||||
by default so reasoning is separate from answer text; `reasoningSplit: false` selects native `<think>`-tagged text.
|
||||
- **Responses:** M3 reasoning defaults off. `reasoningEffort: "none"` disables it; `"minimal"`, `"low"`,
|
||||
`"medium"`, and `"high"` enable reasoning without changing its depth.
|
||||
|
||||
M2.x models always think, even when a disabling option is supplied. For tool continuations, retain the complete
|
||||
`response.message` in history before adding `Message.tool(...)` results; this preserves reasoning and any signatures.
|
||||
|
||||
The default API bases are `https://api.minimax.io/anthropic/v1` for Messages and `https://api.minimax.io/v1` for
|
||||
Chat and Responses. `configure({ baseURL })` replaces the selected API's base, including its version prefix.
|
||||
|
||||
## Meta
|
||||
|
||||
Use Meta's direct [Model API](https://dev.meta.ai/docs/overview) with `META_API_KEY`:
|
||||
|
||||
```ts
|
||||
import { Meta } from "@opencode/ai/providers"
|
||||
|
||||
const meta = Meta.configure() // or Meta.configure({ apiKey })
|
||||
const request = LLM.request({
|
||||
model: meta.responses("muse-spark-1.3"), // meta.model(...) also selects Responses
|
||||
prompt: "What is 173 multiplied by 219? Reply with the integer.",
|
||||
providerOptions: { reasoningEffort: "low" },
|
||||
generation: { maxTokens: 1024 },
|
||||
})
|
||||
```
|
||||
|
||||
`meta.chat("muse-spark-1.3")` selects Chat Completions; `meta.messages("muse-spark-1.3")` selects
|
||||
the Anthropic-compatible Messages API. All use `https://api.meta.ai/v1`. The package entrypoints
|
||||
`@opencode/ai/providers/meta/responses`, `meta/chat`, and `meta/messages` expose `model(modelID, settings)`.
|
||||
|
||||
[Muse Spark](https://dev.meta.ai/docs/models) supports `minimal`, `low`, `medium`, `high`, and
|
||||
`xhigh` reasoning effort; standard-tier 1.3 also supports `max`. Omitting effort uses the model's
|
||||
default. Muse Spark always reasons and rejects `none`. The output-token budget includes private reasoning.
|
||||
|
||||
Responses defaults to `store: false` and `include: ["reasoning.encrypted_content"]`. Preserve
|
||||
`response.message` along with matching `Message.tool(...)` results in subsequent requests to replay
|
||||
reasoning through tool loops. Optional `reasoningSummary: "auto"` requests a readable summary.
|
||||
For server-managed history, override `store: true, include: []` and send the response ID through
|
||||
`http: { body: { previous_response_id: responseID } }` with only the new input.
|
||||
Chat Completions redacts private reasoning and cannot carry it between calls.
|
||||
Responses and Chat support only `toolChoice: "auto"` (the default). Messages also accepts `"none"`;
|
||||
its documented forced `"any"` choice currently returns HTTP 400. Messages defaults to adaptive thinking
|
||||
with `display: "omitted"`, preserving encrypted `redacted_thinking` in `response.message`. Use
|
||||
`providerOptions: { effort: "low" }` for depth or `thinking: { type: "enabled", budgetTokens: 1024 }`
|
||||
for budget compatibility (with `generation.maxTokens > 1024`).
|
||||
|
||||
Add `tools: [Meta.webSearch()]` to a Spark Responses or Messages request for hosted web search.
|
||||
Responses exposes hosted results and URL citations in text-part `providerMetadata.meta.annotations`.
|
||||
To include search result lists, set `include: ["reasoning.encrypted_content", "web_search_call.results"]`.
|
||||
Messages exposes hosted search calls; the recorded Messages API stream does not supply structured
|
||||
citations or separate result blocks. Retain `response.message` for either API's continuation.
|
||||
|
||||
Use `Image.generate` for one-off generation or editing:
|
||||
|
||||
```ts
|
||||
import { Image, ImageInput } from "@opencode/ai"
|
||||
|
||||
const generation = Image.generate({
|
||||
model: meta.image("muse-image-1.0"),
|
||||
prompt: "A flat black square on a white background.",
|
||||
options: { n: 1, reasoningStrength: "low" },
|
||||
})
|
||||
|
||||
const edit = Image.generate({
|
||||
model: meta.image("muse-image-1.0"),
|
||||
prompt: "Make the square purple.",
|
||||
images: [ImageInput.bytes(imageBytes, "image/webp")],
|
||||
options: { outputFormat: "png", reasoningStrength: "low" },
|
||||
})
|
||||
```
|
||||
|
||||
The default image format is WEBP; `outputFormat` also accepts PNG/JPEG and `responseFormat: "url"`
|
||||
returns a signed URL. `size` is an aspect-ratio hint. For conversational images, select
|
||||
`meta.responses("muse-image-1.0")` with `tools: [Meta.imageGeneration({ reasoningStrength: "low" })]`.
|
||||
Generated images are provider-executed tool results with file content. Retain `response.message` to
|
||||
replay the signed image handle on the next request. Muse Image accepts only the `image_generation` tool.
|
||||
|
||||
Meta Responses is explicitly HTTP/SSE-only and does not use WebSockets, even when a caller supplies
|
||||
`StreamOptions.webSocket`. The public `/v1/responses` endpoint rejects WebSocket upgrades with HTTP 405 (`Allow: POST`).
|
||||
|
||||
## Image generation
|
||||
|
||||
Use `Image.generate` with an image model for direct asset generation:
|
||||
|
||||
```ts
|
||||
import { Image, ImageInput } from "@opencode-ai/ai"
|
||||
import { OpenAI } from "@opencode-ai/ai/providers"
|
||||
import { Image, ImageInput } from "@opencode/ai"
|
||||
import { OpenAI } from "@opencode/ai/providers"
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const response = yield* Image.generate({
|
||||
@@ -131,7 +241,7 @@ yield *
|
||||
Google's current Gemini image models use the same direct API:
|
||||
|
||||
```ts
|
||||
import { Google } from "@opencode-ai/ai/providers"
|
||||
import { Google } from "@opencode/ai/providers"
|
||||
|
||||
const googleProgram = Effect.gen(function* () {
|
||||
const response = yield* Image.generate({
|
||||
@@ -207,12 +317,12 @@ The hosted result is represented as a provider-executed tool call and tool resul
|
||||
|
||||
## Testing
|
||||
|
||||
Use the deterministic test client from `@opencode-ai/ai/testing` to script provider-neutral responses and inspect
|
||||
Use the deterministic test client from `@opencode/ai/testing` to script provider-neutral responses and inspect
|
||||
the requests sent by code under test:
|
||||
|
||||
```ts
|
||||
import { Effect } from "effect"
|
||||
import { TestLLM } from "@opencode-ai/ai/testing"
|
||||
import { TestLLM } from "@opencode/ai/testing"
|
||||
|
||||
const programWithTestClient = Effect.gen(function* () {
|
||||
const test = yield* TestLLM.Test
|
||||
@@ -255,7 +365,7 @@ over the same implementation, including the legacy live `requests` array. New te
|
||||
|
||||
## Provider compaction
|
||||
|
||||
Compaction is opt-in. The package supports automatic compaction in OpenAI/Azure Responses and Anthropic Messages (including Claude on Vertex), and explicit compaction calls in OpenAI/Azure/xAI Responses. Model and deployment support still depends on the provider. Bedrock compaction is deferred to a separate follow-up.
|
||||
Compaction is opt-in. The package supports automatic compaction in OpenAI/Azure Responses and Anthropic Messages (including Claude on Vertex), and explicit compaction calls in OpenAI/Azure/xAI Responses. Model and deployment support still depends on the provider.
|
||||
|
||||
This is different from prompt caching, server-side history storage, or truncation. Compaction returns provider-owned context that must be replayed to continue the conversation.
|
||||
|
||||
@@ -298,7 +408,7 @@ result.responseID
|
||||
result.usage
|
||||
```
|
||||
|
||||
This appends a native `compaction_trigger` control item to the full input and sends a normal Responses request. It follows the [Codex V2 request shape](https://github.com/openai/codex/blob/728cb12/codex-rs/core/src/compact_remote_v2_attempt.rs), with tools and instructions retained, `stream: true`, `store: false`, and parallel tool calls enabled. It removes normal-answer text/output-format controls, forced tool choices, output-token/tool-call limits, and automatic `context_management`. Body overlays cannot replace `input` or supply `previous_response_id`/`conversation`; the complete canonical history is required for safe stateless replay. Session/cache identifiers, auth, headers, query parameters, service tier, and supported prompt-cache settings are preserved.
|
||||
This appends a native `compaction_trigger` control item to the full input and sends a normal Responses request, with tools and instructions retained, `stream: true`, `store: false`, and parallel tool calls enabled. It removes normal-answer text/output-format controls, forced tool choices, output-token/tool-call limits, and automatic `context_management`. Body overlays cannot replace `input` or supply `previous_response_id`/`conversation`; the complete canonical history is required for safe stateless replay. Request metadata, auth, headers, query parameters, service tier, and supported prompt-cache settings are preserved.
|
||||
|
||||
Only a successful `response.completed` with a response ID and exactly one logical encrypted checkpoint succeeds. Repeated item events are correlated by ID/output slot, including ID-less checkpoints. Other output is ignored, not returned as assistant text or dispatched as tools. Failed, incomplete, malformed, and interrupted responses return errors rather than partial checkpoints.
|
||||
|
||||
@@ -323,8 +433,8 @@ This capability describes protocol implementation, **not universal availability
|
||||
Inside an `Effect.gen`, enable OpenAI compaction with typed provider options:
|
||||
|
||||
```ts
|
||||
import { LLM, LLMClient, LLMRequest, Message } from "@opencode-ai/ai"
|
||||
import { OpenAI } from "@opencode-ai/ai/providers"
|
||||
import { LLM, LLMClient, LLMRequest, Message } from "@opencode/ai"
|
||||
import { OpenAI } from "@opencode/ai/providers"
|
||||
|
||||
const request = LLM.request({
|
||||
model: OpenAI.configure({ apiKey }).responses("gpt-5.3-codex"),
|
||||
@@ -344,7 +454,7 @@ const next = LLMRequest.update(request, {
|
||||
A compaction part has `provider` and exactly one representation: `encrypted` for Responses, or `text` for Anthropic. Responses also preserves the optional checkpoint `id`. These fields survive message serialization without becoming visible assistant text. Sending a checkpoint to another provider or an incompatible API fails rather than silently losing context.
|
||||
|
||||
```ts
|
||||
import { CompactionPart, ProviderID } from "@opencode-ai/ai"
|
||||
import { CompactionPart, ProviderID } from "@opencode/ai"
|
||||
|
||||
CompactionPart.make({ provider: ProviderID.make("openai"), id: "cmp_123", encrypted: "..." })
|
||||
CompactionPart.make({ provider: ProviderID.make("anthropic"), text: "Summary of the conversation..." })
|
||||
@@ -372,9 +482,7 @@ providerOptions: {
|
||||
- Anthropic can return a compaction block with `content: null` when summarization fails. This becomes a compaction part with `text: null`, which is **not** a successful replacement for prior history. The package never prunes history automatically.
|
||||
- `Usage` totals include all reported Anthropic `usage.iterations`, including compaction. `contextTokens` separately reports the final message iteration's inclusive input size, when available. A compaction-only pause does not report a post-compaction context size. Raw iteration usage remains in `providerMetadata`.
|
||||
|
||||
### Ownership and verification
|
||||
|
||||
The AI package transports options and typed conversation parts. It does not schedule compaction, persist Session checkpoints, select history, switch providers, or replace Core's existing local compaction policy. Native compaction is not enabled for OpenCode Sessions by this feature; Session integration must persist these parts before enabling it. The AI SDK bridge rejects native compaction parts rather than dropping them. Provider-executed tool APIs and persistence changes are a separate follow-up.
|
||||
### Recording tests
|
||||
|
||||
Tests cover serialized round trips, real local HTTP plus a tool loop, WebSocket recovery, provider errors, malformed blocks, and usage accounting. Live provider tests are gated by `RECORD=true` and the relevant API keys:
|
||||
|
||||
@@ -393,7 +501,7 @@ Prompt caching is **on by default**. Every `LLMRequest` resolves to `cache: "aut
|
||||
|
||||
### Auto placement
|
||||
|
||||
`"auto"` places up to four breakpoints — the last tool definition, the first system part, the last system part when distinct, and the final message boundary. These expose successively larger reusable prefixes for tools, the base agent, project instructions, and the active conversation. The rolling final-message boundary advances on every request so recent conversation prefixes remain reusable during tool loops.
|
||||
`"auto"` places up to four breakpoints — the last tool definition, the first system part, the last system part when distinct, and the final message boundary. These expose successively larger reusable prefixes for tool definitions, system instructions, and the active conversation. The rolling final-message boundary advances on every request so recent conversation prefixes remain reusable during tool loops.
|
||||
|
||||
Tools precede every system and conversation block in the provider prefix, so tool definitions must remain byte-stable and deterministically ordered for downstream breakpoints to remain reusable.
|
||||
|
||||
@@ -452,7 +560,7 @@ Normalized cache usage is read back into `response.usage.cacheReadInputTokens` a
|
||||
Provider facades configure endpoint/auth/deployment details first, then expose model selectors that take only a model or deployment id. The selected model carries the executable route value used at runtime.
|
||||
|
||||
```ts
|
||||
import { OpenAI, CloudflareAIGateway } from "@opencode-ai/ai/providers"
|
||||
import { OpenAI, CloudflareAIGateway } from "@opencode/ai/providers"
|
||||
|
||||
const openai = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses("gpt-4o-mini")
|
||||
const gateway = CloudflareAIGateway.configure({
|
||||
@@ -466,7 +574,7 @@ Included LLM providers: OpenAI, Anthropic, Google (Gemini), Google Vertex, Amazo
|
||||
Each named provider owns its module, endpoint, authentication, and route setup. Providers with the same wire format compose the shared protocol directly:
|
||||
|
||||
```ts
|
||||
import { DeepSeek, Fireworks } from "@opencode-ai/ai/providers"
|
||||
import { DeepSeek, Fireworks } from "@opencode/ai/providers"
|
||||
|
||||
const deepseek = DeepSeek.configure({ apiKey }).model("deepseek-chat")
|
||||
const fireworks = Fireworks.configure({ apiKey }).model("accounts/fireworks/models/my-model")
|
||||
@@ -474,29 +582,29 @@ const fireworks = Fireworks.configure({ apiKey }).model("accounts/fireworks/mode
|
||||
|
||||
The former `OpenAICompatible.baseten`, `.cerebras`, `.deepinfra`, `.deepseek`, `.fireworks`, `.groq`, and `.togetherai` presets are replaced by the top-level `Baseten`, `Cerebras`, `DeepInfra`, `DeepSeek`, `Fireworks`, `Groq`, and `TogetherAI` exports. Use `CloudflareAIGateway` and `CloudflareWorkersAI` directly; each has its own module. `OpenAICompatible` configures generic endpoints with an explicit `baseURL`.
|
||||
|
||||
### Package-like entrypoints
|
||||
### Provider entrypoints
|
||||
|
||||
Native catalog integrations load provider behavior through package-like entrypoints. These are export paths from the same `@opencode-ai/ai` npm package, not independently published packages. Each entrypoint exports the same `model(modelID, settings)` contract, and `settings` contains serializable provider configuration plus common `headers` and `body` overlays.
|
||||
Provider modules are available through dedicated exports from `@opencode/ai`. Each LLM entrypoint exports `model(modelID, settings)`, where `settings` contains provider configuration plus common `headers` and `body` overlays.
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/openai/responses"
|
||||
import { model } from "@opencode/ai/providers/openai/responses"
|
||||
|
||||
const selected = model("gpt-5", {
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
headers: { "x-application": "opencode" },
|
||||
headers: { "x-application": "example" },
|
||||
})
|
||||
```
|
||||
|
||||
OpenAI Chat and OpenAI Responses are separate semantic entrypoints:
|
||||
APIs have separate entrypoints:
|
||||
|
||||
- `@opencode-ai/ai/providers/openai/chat`
|
||||
- `@opencode-ai/ai/providers/openai/responses`
|
||||
- `@opencode-ai/ai/providers/openai-compatible/responses`
|
||||
- `@opencode-ai/ai/providers/anthropic-compatible`
|
||||
- `@opencode-ai/ai/providers/google-vertex/gemini`
|
||||
- `@opencode-ai/ai/providers/google-vertex/chat`
|
||||
- `@opencode-ai/ai/providers/google-vertex/responses`
|
||||
- `@opencode-ai/ai/providers/google-vertex/messages`
|
||||
- `@opencode/ai/providers/openai/chat`
|
||||
- `@opencode/ai/providers/openai/responses`
|
||||
- `@opencode/ai/providers/openai-compatible/responses`
|
||||
- `@opencode/ai/providers/anthropic-compatible`
|
||||
- `@opencode/ai/providers/google-vertex/gemini`
|
||||
- `@opencode/ai/providers/google-vertex/chat`
|
||||
- `@opencode/ai/providers/google-vertex/responses`
|
||||
- `@opencode/ai/providers/google-vertex/messages`
|
||||
|
||||
OpenAI Responses has one semantic route and uses HTTP by default. Advanced callers may supply a per-call WebSocket channel executor through `StreamOptions`; transport policy does not change provider settings, model identity, or route identity. The provider-neutral Open Responses implementation owns the reusable WebSocket request and event contract, while each provider opts in with its own handshake and connection policy. Azure follows the same Chat/Responses split at `providers/azure/chat` and `providers/azure/responses`. Generic OpenAI-compatible Chat remains at `providers/openai-compatible`; the Responses adapter at `providers/openai-compatible/responses` uses the provider-neutral Open Responses protocol. OpenAI Responses extends that baseline with OpenAI tools, event variants, metadata, and defaults. Generic Anthropic Messages-compatible providers use `providers/anthropic-compatible`, which the named Anthropic provider composes. Google Gemini and Amazon Bedrock expose their single native API through their existing provider paths.
|
||||
|
||||
@@ -505,40 +613,36 @@ Vertex Gemini, Vertex Chat, Vertex Responses, and Vertex Messages are separate A
|
||||
Tuned Vertex Gemini deployments use model ids shaped like `endpoints/1234567890` and require OAuth or ADC; Vertex express-mode API keys support publisher models only.
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/gemini"
|
||||
import { model } from "@opencode/ai/providers/google-vertex/gemini"
|
||||
|
||||
model("gemini-3.5-flash", { project: "my-project", location: "global" })
|
||||
```
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/chat"
|
||||
import { model } from "@opencode/ai/providers/google-vertex/chat"
|
||||
|
||||
model("deepseek-ai/deepseek-v3.2-maas", { project: "my-project", location: "global" })
|
||||
```
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/responses"
|
||||
import { model } from "@opencode/ai/providers/google-vertex/responses"
|
||||
|
||||
model("xai/grok-4.20-reasoning", { project: "my-project", location: "global" })
|
||||
```
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/messages"
|
||||
import { model } from "@opencode/ai/providers/google-vertex/messages"
|
||||
|
||||
model("claude-sonnet-4-6", { project: "my-project", location: "global" })
|
||||
```
|
||||
|
||||
Provider facades such as `OpenAI.configure(...).responses(...)` remain the direct application API. Package-like entrypoints are the self-similar loading contract used when a catalog selects behavior by export path.
|
||||
Additional provider entrypoints include:
|
||||
|
||||
Every named LLM provider listed above also exports `model(modelID, settings)` from its own package entrypoint. The extracted providers are available at:
|
||||
|
||||
- `@opencode-ai/ai/providers/baseten`
|
||||
- `@opencode-ai/ai/providers/deepseek`
|
||||
- `@opencode-ai/ai/providers/fireworks`
|
||||
- `@opencode-ai/ai/providers/cloudflare-ai-gateway`
|
||||
- `@opencode-ai/ai/providers/cloudflare-workers-ai`
|
||||
|
||||
Core resolves known compatible catalog providers through their dedicated entrypoints, including the `fireworks-ai` catalog ID through `providers/fireworks`.
|
||||
- `@opencode/ai/providers/baseten`
|
||||
- `@opencode/ai/providers/deepseek`
|
||||
- `@opencode/ai/providers/fireworks`
|
||||
- `@opencode/ai/providers/cloudflare-ai-gateway`
|
||||
- `@opencode/ai/providers/cloudflare-workers-ai`
|
||||
|
||||
## Provider options & HTTP overlays
|
||||
|
||||
@@ -565,7 +669,7 @@ LLM.request({
|
||||
|
||||
## Routes
|
||||
|
||||
Adding a new model or deployment is usually 5-15 lines using `Route.make({ protocol, endpoint, auth, framing, ... })`. The route owns endpoint/auth/framing and the protocol owns body construction plus stream parsing. Transports are reusable IO templates that receive route endpoint/auth at compile time. Capability/catalog metadata lives outside this low-level package; unsupported request shapes fail during protocol lowering. See `AGENTS.md` for the architectural detail.
|
||||
Compose a route with `Route.make({ protocol, endpoint, auth, framing, ... })`. The route owns endpoint/auth/framing and the protocol owns body construction plus stream parsing. Transports receive the route's endpoint and auth when preparing requests. Unsupported request shapes fail during protocol lowering.
|
||||
|
||||
## Effect
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Config, Effect, Formatter, Layer, Schema, Stream } from "effect"
|
||||
import { LLM, LLMClient, LLMRequest, Message, ProviderID, Tool, ToolRuntime } from "@opencode-ai/ai"
|
||||
import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor } from "@opencode-ai/ai/route"
|
||||
import { OpenAI } from "@opencode-ai/ai/providers"
|
||||
import { LLM, LLMClient, LLMRequest, Message, ProviderID, Tool, ToolRuntime } from "@opencode/ai"
|
||||
import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor } from "@opencode/ai/route"
|
||||
import { OpenAI } from "@opencode/ai/providers"
|
||||
|
||||
/**
|
||||
* A runnable walkthrough of the LLM package use-site API.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.20",
|
||||
"name": "@opencode-ai/ai",
|
||||
"name": "@opencode/ai",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@@ -21,7 +21,7 @@
|
||||
"devDependencies": {
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/http-recorder": "workspace:*",
|
||||
"@opencode/http-recorder": "workspace:*",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
"aws4fetch": "1.0.20",
|
||||
"effect": "catalog:",
|
||||
"google-auth-library": "10.5.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bun
|
||||
import { Script } from "@opencode-ai/script"
|
||||
import { Script } from "@opencode/script"
|
||||
import { $ } from "bun"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Buffer } from "node:buffer"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import { Route } from "../route/client.js"
|
||||
import { Auth } from "../route/auth.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import { Route } from "../route/client.js"
|
||||
import { Auth } from "../route/auth.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Effect, Encoding, Schema } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { GeneratedImage, ImageModel, ImageResponse, type ImageRequestFor, type ImageRoute } from "../image.js"
|
||||
import { Auth } from "../route/auth.js"
|
||||
import { Usage, mergeHttpOptions, mergeJsonRecords, type HttpOptions } from "../schema/index.js"
|
||||
import { JsonObject, ProviderShared, optionalNull } from "./shared.js"
|
||||
import { ImageInputs } from "./utils/image-input.js"
|
||||
|
||||
type OpenString<Known extends string> = Known | (string & {})
|
||||
export type ImageOptions = {
|
||||
readonly n?: number
|
||||
/** Aspect ratio hint, not an exact output resolution. */
|
||||
readonly size?: string
|
||||
readonly outputFormat?: OpenString<"webp" | "png" | "jpeg">
|
||||
readonly responseFormat?: OpenString<"b64_json" | "url">
|
||||
readonly reasoningStrength?: OpenString<"low" | "high">
|
||||
readonly toolEnablement?: {
|
||||
readonly enable_image_search?: boolean
|
||||
readonly enable_web_search?: boolean
|
||||
readonly enable_shell?: boolean
|
||||
}
|
||||
readonly [key: string]: unknown
|
||||
}
|
||||
|
||||
const Body = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
model: Schema.String,
|
||||
prompt: Schema.String,
|
||||
images: Schema.optional(Schema.Array(JsonObject)),
|
||||
n: Schema.optional(Schema.Number),
|
||||
size: Schema.optional(Schema.String),
|
||||
output_format: Schema.optional(Schema.String),
|
||||
response_format: Schema.optional(Schema.String),
|
||||
reasoning_strength: Schema.optional(Schema.String),
|
||||
tool_enablement: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
|
||||
}),
|
||||
[JsonObject],
|
||||
)
|
||||
|
||||
const Response = Schema.Struct({
|
||||
data: Schema.Array(Schema.Struct({ b64_json: optionalNull(Schema.String), url: optionalNull(Schema.String) })),
|
||||
output_format: Schema.optional(Schema.String),
|
||||
usage: Schema.optional(
|
||||
Schema.Struct({
|
||||
input_tokens: Schema.optional(Schema.Number),
|
||||
output_tokens: Schema.optional(Schema.Number),
|
||||
total_tokens: Schema.optional(Schema.Number),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
export const model = (input: {
|
||||
readonly id: string
|
||||
readonly auth: Auth.Definition
|
||||
readonly baseURL: string
|
||||
readonly headers?: Record<string, string>
|
||||
readonly http?: HttpOptions
|
||||
}) => {
|
||||
const route: ImageRoute<ImageOptions> = {
|
||||
id: "meta-images",
|
||||
generate: Effect.fn("MetaImages.generate")(function* (request: ImageRequestFor<ImageOptions>, execute) {
|
||||
const http = mergeHttpOptions(request.model.http, request.http)
|
||||
const images = yield* Effect.forEach(request.images ?? [], (image) => {
|
||||
if (image.type === "bytes") return Effect.succeed({ image_url: ImageInputs.dataUrl(image) })
|
||||
if (image.type === "url") return Effect.succeed({ image_url: image.url })
|
||||
return ImageInputs.invalid("Meta Images accepts image bytes and URLs")
|
||||
})
|
||||
const { outputFormat, responseFormat, reasoningStrength, toolEnablement, ...native } = request.options ?? {}
|
||||
const payload = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))(
|
||||
mergeJsonRecords(
|
||||
{
|
||||
model: request.model.id,
|
||||
prompt: request.prompt,
|
||||
images: images.length === 0 ? undefined : images,
|
||||
output_format: outputFormat,
|
||||
response_format: responseFormat,
|
||||
reasoning_strength: reasoningStrength,
|
||||
tool_enablement: toolEnablement,
|
||||
},
|
||||
native,
|
||||
http?.body,
|
||||
),
|
||||
)
|
||||
const body = ProviderShared.encodeJson(payload)
|
||||
const url = new URL(`${input.baseURL.replace(/\/$/, "")}/images/${images.length === 0 ? "generations" : "edits"}`)
|
||||
Object.entries(http?.query ?? {}).forEach(([key, value]) => url.searchParams.set(key, value))
|
||||
const headers = yield* Auth.toEffect(input.auth)({
|
||||
request,
|
||||
method: "POST",
|
||||
url: url.toString(),
|
||||
body,
|
||||
headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
|
||||
})
|
||||
const response = yield* execute(
|
||||
HttpClientRequest.post(url.toString()).pipe(
|
||||
HttpClientRequest.setHeaders(headers),
|
||||
HttpClientRequest.bodyText(body, "application/json"),
|
||||
),
|
||||
)
|
||||
const output = yield* ProviderShared.imageResponse("meta-images", "Meta Images", response)
|
||||
const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Response))(output.body).pipe(
|
||||
Effect.mapError((cause) => output.invalid("Meta Images returned an invalid response", cause)),
|
||||
)
|
||||
const format = decoded.output_format ?? payload.output_format ?? "webp"
|
||||
const generated = yield* Effect.forEach(decoded.data, (item, index) => {
|
||||
if (item.b64_json)
|
||||
return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(
|
||||
Effect.mapError((cause) => output.invalid(`Meta Images result ${index} contains invalid base64`, cause)),
|
||||
Effect.map((data) => new GeneratedImage({ mediaType: `image/${format}`, data })),
|
||||
)
|
||||
if (item.url) return Effect.succeed(new GeneratedImage({ mediaType: `image/${format}`, data: item.url }))
|
||||
return output.invalid(`Meta Images result ${index} has neither image data nor a URL`)
|
||||
})
|
||||
if (generated.length === 0) return yield* output.invalid("Meta Images returned no images")
|
||||
return new ImageResponse({
|
||||
images: generated,
|
||||
usage:
|
||||
decoded.usage === undefined
|
||||
? undefined
|
||||
: new Usage({
|
||||
inputTokens: decoded.usage.input_tokens,
|
||||
outputTokens: decoded.usage.output_tokens,
|
||||
totalTokens: decoded.usage.total_tokens,
|
||||
providerMetadata: { meta: decoded.usage },
|
||||
}),
|
||||
providerMetadata: { meta: { outputFormat: format } },
|
||||
})
|
||||
}),
|
||||
}
|
||||
return ImageModel.make<ImageOptions>({ id: input.id, provider: "meta", route, http: input.http })
|
||||
}
|
||||
|
||||
export * as MetaImages from "./meta-images.js"
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import type { LLMRequest } from "../schema/index.js"
|
||||
import { AnthropicMessages } from "./anthropic-messages.js"
|
||||
import { MetaResponses } from "./meta-responses.js"
|
||||
import { JsonObject, optionalArray, ProviderShared } from "./shared.js"
|
||||
|
||||
const WebSearch = Schema.Struct({
|
||||
type: Schema.Literal("web_search"),
|
||||
name: Schema.Literal("web_search"),
|
||||
user_location: MetaResponses.WebSearch.fields.user_location,
|
||||
})
|
||||
const Body = Schema.Struct({
|
||||
...AnthropicMessages.AnthropicMessagesBody.fields,
|
||||
tools: optionalArray(
|
||||
Schema.Union([
|
||||
Schema.Struct({ name: Schema.String, description: Schema.String, input_schema: JsonObject }),
|
||||
WebSearch,
|
||||
]),
|
||||
),
|
||||
})
|
||||
|
||||
const fromRequest = Effect.fn("MetaMessages.fromRequest")(function* (request: LLMRequest) {
|
||||
const projected = ProviderShared.flattenToolRequest(request)
|
||||
const body = yield* AnthropicMessages.protocol.body.from(projected.request)
|
||||
return {
|
||||
...body,
|
||||
tools:
|
||||
body.tools === undefined
|
||||
? undefined
|
||||
: yield* Effect.forEach(body.tools, (tool, index) =>
|
||||
Effect.gen(function* () {
|
||||
const native = projected.tools[index]?.native
|
||||
if (native === undefined) return tool
|
||||
const search = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(MetaResponses.WebSearch))(
|
||||
native.meta,
|
||||
)
|
||||
if (search.search_context_size !== undefined)
|
||||
return yield* ProviderShared.invalidRequest("Meta Messages does not support searchContextSize")
|
||||
return { type: "web_search" as const, name: "web_search" as const, user_location: search.user_location }
|
||||
}),
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
export const protocol = Protocol.make({
|
||||
id: "meta-messages",
|
||||
body: { schema: Body, from: fromRequest },
|
||||
stream: AnthropicMessages.protocol.stream,
|
||||
})
|
||||
|
||||
export * as MetaMessages from "./meta-messages.js"
|
||||
@@ -0,0 +1,238 @@
|
||||
import { Effect, Encoding, Schema } from "effect"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { HttpTransport } from "../route/transport/index.js"
|
||||
import { LLMEvent, LLMRequest, Message, ToolResultPart } from "../schema/index.js"
|
||||
import { OpenResponses } from "./open-responses.js"
|
||||
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
|
||||
import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js"
|
||||
import { ToolSchemaProjection } from "./utils/tool-schema.js"
|
||||
import { MetaImage } from "./utils/meta-image.js"
|
||||
|
||||
const ADAPTER = "meta-responses"
|
||||
const NAME = "Meta Responses"
|
||||
|
||||
export const WebSearch = Schema.Struct({
|
||||
type: Schema.Literal("web_search"),
|
||||
search_context_size: Schema.optional(Schema.String),
|
||||
user_location: Schema.optional(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("approximate"),
|
||||
city: Schema.optional(Schema.String),
|
||||
region: Schema.optional(Schema.String),
|
||||
country: Schema.optional(Schema.String),
|
||||
timezone: Schema.optional(Schema.String),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
export const ImageGeneration = Schema.Struct({
|
||||
type: Schema.Literal("image_generation"),
|
||||
size: Schema.optional(Schema.String),
|
||||
output_format: Schema.optional(Schema.String),
|
||||
reasoning_strength: Schema.optional(Schema.String),
|
||||
enable_image_search: Schema.optional(Schema.Boolean),
|
||||
enable_web_search: Schema.optional(Schema.Boolean),
|
||||
enable_shell: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
|
||||
const NativeTool = Schema.Union([WebSearch, ImageGeneration])
|
||||
const ImageItem = Schema.Struct({
|
||||
type: Schema.Literal("image_generation_call"),
|
||||
id: Schema.String,
|
||||
status: Schema.optional(Schema.String),
|
||||
result: optionalNull(Schema.String),
|
||||
output_format: Schema.optional(Schema.String),
|
||||
error: Schema.optional(Schema.Unknown),
|
||||
})
|
||||
|
||||
const Body = Schema.Struct({
|
||||
...OpenResponses.coreFields,
|
||||
input: Schema.Array(Schema.Union([OpenResponses.InputItem, ImageItem])),
|
||||
tools: optionalArray(Schema.Union([OpenResponses.Tool, NativeTool])),
|
||||
stream: Schema.Literal(true),
|
||||
})
|
||||
|
||||
const MessageAnnotations = Schema.Struct({
|
||||
content: Schema.Array(Schema.Struct({ annotations: optionalArray(JsonObject) })),
|
||||
})
|
||||
|
||||
interface ParserState extends OpenResponses.ParserState {
|
||||
readonly completedItems: ReadonlySet<string>
|
||||
}
|
||||
|
||||
const adapter = {
|
||||
id: ADAPTER,
|
||||
name: NAME,
|
||||
restoreHostedToolItem: (item: unknown) => (Schema.is(ImageItem)(item) ? item : undefined),
|
||||
} satisfies OpenResponses.ProviderAdapter
|
||||
|
||||
const fromRequest = Effect.fn("MetaResponses.fromRequest")(function* (request: LLMRequest) {
|
||||
const key = request.model.route.providerMetadataKey ?? String(request.model.provider)
|
||||
const projected = ProviderShared.flattenToolRequest(
|
||||
LLMRequest.update(request, {
|
||||
messages: request.messages.map((message) =>
|
||||
Message.make({
|
||||
...message,
|
||||
content: message.content.map((part) => {
|
||||
if (
|
||||
part.type !== "tool-result" ||
|
||||
!part.providerExecuted ||
|
||||
part.name !== "image_generation" ||
|
||||
part.result.type !== "content" ||
|
||||
part.providerMetadata?.[key]?.itemId !== part.id
|
||||
)
|
||||
return part
|
||||
// Meta's signed image ID carries edit state; replay the handle, not the image bytes as a user message.
|
||||
return ToolResultPart.make({
|
||||
...part,
|
||||
result: {
|
||||
type: "json",
|
||||
value: { type: "image_generation_call", id: part.id, status: "completed", result: null },
|
||||
},
|
||||
})
|
||||
}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))({
|
||||
...(yield* OpenResponses.lowerConversation(projected.request, adapter)),
|
||||
...OpenResponses.lowerGeneration(request),
|
||||
tools:
|
||||
projected.tools.length === 0
|
||||
? undefined
|
||||
: yield* Effect.forEach(projected.tools, (tool) =>
|
||||
Effect.gen(function* () {
|
||||
if (tool.native === undefined)
|
||||
return yield* OpenResponses.lowerTool(
|
||||
NAME,
|
||||
tool,
|
||||
ToolSchemaProjection.modelCompatibility(tool.inputSchema, request.model.compatibility?.toolSchema),
|
||||
)
|
||||
return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(NativeTool))(tool.native.meta)
|
||||
}),
|
||||
),
|
||||
tool_choice:
|
||||
OpenResponses.allowedToolChoice(request) ??
|
||||
(request.toolChoice ? yield* OpenResponses.lowerToolChoice(NAME, request.toolChoice) : undefined),
|
||||
})
|
||||
})
|
||||
|
||||
const HOSTED_TOOLS = {
|
||||
web_search_call: { name: "web_search", input: (item) => item.action ?? {} },
|
||||
image_generation_call: {
|
||||
name: "image_generation",
|
||||
input: () => ({}),
|
||||
result: Effect.fn("MetaResponses.imageResult")(function* (raw: ResponsesHostedTools.Item) {
|
||||
const item = yield* Schema.decodeUnknownEffect(ImageItem)(raw).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
"Meta returned an invalid image item",
|
||||
ProviderShared.encodeJson(raw),
|
||||
cause,
|
||||
),
|
||||
),
|
||||
)
|
||||
if (item.error !== undefined && item.error !== null) return { type: "error" as const, value: item.error }
|
||||
if (!item.result)
|
||||
return yield* ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
"Meta returned an image without data",
|
||||
ProviderShared.encodeJson(raw),
|
||||
)
|
||||
const data = yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
"Meta returned invalid image base64",
|
||||
ProviderShared.encodeJson(raw),
|
||||
cause,
|
||||
),
|
||||
),
|
||||
)
|
||||
const mime = MetaImage.mediaType(data, item.output_format)
|
||||
return {
|
||||
type: "content" as const,
|
||||
value: [{ type: "file" as const, uri: `data:${mime};base64,${item.result}`, mime }],
|
||||
}
|
||||
}),
|
||||
},
|
||||
} satisfies ResponsesHostedTools.Definitions
|
||||
|
||||
const onEvent = Effect.fn("MetaResponses.onEvent")(function* (
|
||||
state: OpenResponses.ParserState,
|
||||
input: OpenResponses.Event,
|
||||
) {
|
||||
const event = OpenResponses.normalize(state, input)
|
||||
if (event.type === "response.output_item.done" && event.item && ResponsesHostedTools.isItem(event.item, HOSTED_TOOLS))
|
||||
return yield* ResponsesHostedTools.onDone(state, event.item, HOSTED_TOOLS)
|
||||
const result = yield* OpenResponses.step(state, event)
|
||||
if (event.type !== "response.output_item.done" || event.item?.type !== "message") return result
|
||||
const message = yield* Schema.decodeUnknownEffect(MessageAnnotations)(event.item).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
"Meta returned invalid message annotations",
|
||||
ProviderShared.encodeJson(event),
|
||||
cause,
|
||||
),
|
||||
),
|
||||
)
|
||||
const annotations = message.content.flatMap((part) => part.annotations ?? [])
|
||||
if (annotations.length === 0) return result
|
||||
return [
|
||||
result[0],
|
||||
result[1].map((item) =>
|
||||
LLMEvent.is.textEnd(item)
|
||||
? LLMEvent.textEnd({
|
||||
...item,
|
||||
providerMetadata: {
|
||||
...item.providerMetadata,
|
||||
[state.providerMetadataKey]: { ...item.providerMetadata?.[state.providerMetadataKey], annotations },
|
||||
},
|
||||
})
|
||||
: item,
|
||||
),
|
||||
] satisfies OpenResponses.StepResult
|
||||
})
|
||||
|
||||
const step = Effect.fn("MetaResponses.step")(function* (state: ParserState, input: OpenResponses.Event) {
|
||||
const completedItems = new Set(state.completedItems)
|
||||
const event = OpenResponses.normalize(state, input)
|
||||
if (event.type === "response.output_item.done" && event.item && completedItems.has(event.item.id))
|
||||
return [state, []] as const
|
||||
const events: LLMEvent[] = []
|
||||
let current: OpenResponses.ParserState = state
|
||||
// Muse Image delivers its image and optional summary only in response.completed.
|
||||
// Recover terminal-only items in order, without duplicating Spark's streamed items.
|
||||
if (event.type === "response.completed") {
|
||||
for (const [index, item] of (event.response?.output ?? []).entries()) {
|
||||
const done = OpenResponses.normalize(current, { type: "response.output_item.done", item, output_index: index })
|
||||
// Spark changes reasoning IDs in the terminal snapshot; output indices still identify the streamed items.
|
||||
if (!done.item || completedItems.has(done.item.id) || completedItems.has(state.outputItems[index] ?? "")) continue
|
||||
const result = yield* onEvent(current, done)
|
||||
current = result[0]
|
||||
events.push(...result[1])
|
||||
completedItems.add(done.item.id)
|
||||
}
|
||||
}
|
||||
const result = yield* onEvent(current, event)
|
||||
if (event.type === "response.output_item.done" && event.item) completedItems.add(event.item.id)
|
||||
return [{ ...result[0], completedItems }, [...events, ...result[1]]] as const
|
||||
})
|
||||
|
||||
export const protocol = Protocol.make({
|
||||
id: ADAPTER,
|
||||
body: { schema: Body, from: fromRequest },
|
||||
stream: {
|
||||
event: OpenResponses.protocol.stream.event,
|
||||
initial: (request): ParserState => ({ ...OpenResponses.initial(request, adapter), completedItems: new Set() }),
|
||||
step,
|
||||
terminal: OpenResponses.terminal,
|
||||
},
|
||||
})
|
||||
|
||||
export const httpTransport = HttpTransport.sseJson.with<Schema.Schema.Type<typeof Body>>()
|
||||
|
||||
export * as MetaResponses from "./meta-responses.js"
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import type { Content } from "@opencode-ai/schema/tool"
|
||||
import type { Content } from "@opencode/schema/tool"
|
||||
import { HttpTransport } from "../route/transport/index.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import { Route } from "../route/client.js"
|
||||
import { Auth } from "../route/auth.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Buffer } from "node:buffer"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import * as Sse from "effect/unstable/encoding/Sse"
|
||||
import { Headers, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Responses image items can omit output_format, including when PNG/JPEG was requested.
|
||||
export const mediaType = (data: Uint8Array, format?: string) => {
|
||||
if (format !== undefined) return `image/${format}`
|
||||
if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png"
|
||||
if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg"
|
||||
if (new TextDecoder().decode(data.slice(0, 4)) === "RIFF" && new TextDecoder().decode(data.slice(8, 12)) === "WEBP")
|
||||
return "image/webp"
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
export * as MetaImage from "./meta-image.js"
|
||||
@@ -16,6 +16,8 @@ export * as GoogleVertexChat from "./google-vertex-chat.js"
|
||||
export * as GoogleVertexMessages from "./google-vertex-messages.js"
|
||||
export * as GoogleVertexResponses from "./google-vertex-responses.js"
|
||||
export * as Groq from "./groq.js"
|
||||
export * as Meta from "./meta.js"
|
||||
export * as MiniMax from "./minimax.js"
|
||||
export * as Mistral from "./mistral.js"
|
||||
export * as OpenAI from "./openai.js"
|
||||
export * as OpenAICompatible from "./openai-compatible.js"
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { OpenAIChat } from "../protocols/openai-chat.js"
|
||||
import { AnthropicMessages } from "../protocols/anthropic-messages.js"
|
||||
import { MetaResponses } from "../protocols/meta-responses.js"
|
||||
import { MetaMessages } from "../protocols/meta-messages.js"
|
||||
import { MetaImages } from "../protocols/meta-images.js"
|
||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
|
||||
import { Route, type RouteDefaultsInput } from "../route/client.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { HttpOptions, ProviderID, ToolDefinition, type ModelID } from "../schema/index.js"
|
||||
import type { OpenResponsesProviderOptionsInput } from "./open-responses-options.js"
|
||||
|
||||
export const id = ProviderID.make("meta")
|
||||
const baseURL = "https://api.meta.ai/v1"
|
||||
|
||||
export type ProviderOptionsInput = OpenResponsesProviderOptionsInput &
|
||||
Pick<AnthropicMessages.OptionsInput, "thinking" | "effort">
|
||||
export type MessagesOptionsInput = Pick<
|
||||
AnthropicMessages.OptionsInput,
|
||||
"thinking" | "effort" | "outputConfig" | "output_config" | "serviceTier" | "service_tier" | "metadata"
|
||||
> & { readonly [key: string]: unknown }
|
||||
export type ImageOptions = MetaImages.ImageOptions
|
||||
|
||||
export interface WebSearchOptions {
|
||||
readonly searchContextSize?: "low" | "medium" | "high" | (string & {})
|
||||
readonly userLocation?: {
|
||||
readonly city?: string
|
||||
readonly region?: string
|
||||
readonly country?: string
|
||||
readonly timezone?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const webSearch = (options: WebSearchOptions = {}) =>
|
||||
ToolDefinition.make({
|
||||
name: "web_search",
|
||||
description: "Search the web with Meta's hosted search tool.",
|
||||
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
||||
native: {
|
||||
meta: {
|
||||
type: "web_search",
|
||||
search_context_size: options.searchContextSize,
|
||||
user_location:
|
||||
options.userLocation === undefined ? undefined : { type: "approximate", ...options.userLocation },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export interface ImageGenerationOptions {
|
||||
readonly size?: string
|
||||
readonly outputFormat?: "webp" | "png" | "jpeg" | (string & {})
|
||||
readonly reasoningStrength?: "low" | "high" | (string & {})
|
||||
readonly enableImageSearch?: boolean
|
||||
readonly enableWebSearch?: boolean
|
||||
readonly enableShell?: boolean
|
||||
}
|
||||
|
||||
export const imageGeneration = (options: ImageGenerationOptions = {}) =>
|
||||
ToolDefinition.make({
|
||||
name: "image_generation",
|
||||
description: "Generate or edit an image with Muse Image.",
|
||||
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
||||
native: {
|
||||
meta: {
|
||||
type: "image_generation",
|
||||
size: options.size,
|
||||
output_format: options.outputFormat,
|
||||
reasoning_strength: options.reasoningStrength,
|
||||
enable_image_search: options.enableImageSearch,
|
||||
enable_web_search: options.enableWebSearch,
|
||||
enable_shell: options.enableShell,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: ProviderOptionsInput
|
||||
}
|
||||
|
||||
export interface Settings extends ProviderPackage.Settings {
|
||||
readonly apiKey?: string
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: ProviderOptionsInput
|
||||
}
|
||||
|
||||
const responsesRoute = Route.make({
|
||||
id: "meta-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "meta",
|
||||
protocol: MetaResponses.protocol,
|
||||
endpoint: Endpoint.path("/responses", { baseURL }),
|
||||
// Meta Responses does not support WebSocket upgrades; always use HTTP/SSE.
|
||||
transport: MetaResponses.httpTransport,
|
||||
defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
|
||||
})
|
||||
|
||||
const chatRoute = Route.make({
|
||||
id: "meta-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "meta",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
})
|
||||
|
||||
const messagesRoute = Route.make({
|
||||
id: "meta-messages",
|
||||
provider: id,
|
||||
providerMetadataKey: "meta",
|
||||
protocol: MetaMessages.protocol,
|
||||
endpoint: Endpoint.path("/messages", { baseURL }),
|
||||
framing: AnthropicMessages.framing,
|
||||
defaults: { providerOptions: { thinking: { type: "adaptive", display: "omitted" } } },
|
||||
})
|
||||
|
||||
export const routes = [responsesRoute, chatRoute, messagesRoute]
|
||||
|
||||
export const configure = (input: LanguageModelOptions = {}) => {
|
||||
const { apiKey: _apiKey, auth: _auth, baseURL: endpoint, ...defaults } = input
|
||||
const options = {
|
||||
...defaults,
|
||||
endpoint: { baseURL: endpoint ?? baseURL },
|
||||
auth: AuthOptions.bearer(input, "META_API_KEY"),
|
||||
}
|
||||
const configuredResponses = responsesRoute.with(options)
|
||||
const configuredChat = chatRoute.with(options)
|
||||
const configuredMessages = messagesRoute.with(options)
|
||||
const responses = (modelID: string | ModelID) =>
|
||||
configuredResponses.model<OpenResponsesProviderOptionsInput>({ id: modelID })
|
||||
const chat = (modelID: string | ModelID) =>
|
||||
configuredChat.model<OpenResponsesProviderOptionsInput>({
|
||||
id: modelID,
|
||||
compatibility: { maxTokensField: "max_completion_tokens", supportsStore: false },
|
||||
})
|
||||
const messages = (modelID: string | ModelID) =>
|
||||
configuredMessages.model<MessagesOptionsInput>({
|
||||
id: modelID,
|
||||
compatibility: { requireSignature: false },
|
||||
})
|
||||
const image = (modelID: string | ModelID) =>
|
||||
MetaImages.model({
|
||||
id: modelID,
|
||||
baseURL: endpoint ?? baseURL,
|
||||
auth: options.auth,
|
||||
headers: input.headers,
|
||||
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
|
||||
})
|
||||
return { id, model: responses, responses, chat, messages, image, configure }
|
||||
}
|
||||
|
||||
export const provider = configure()
|
||||
export const responses = provider.responses
|
||||
export const chat = provider.chat
|
||||
export const messages = provider.messages
|
||||
export const image = provider.image
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, OpenResponsesProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => fromSettings(settings).responses(modelID)
|
||||
|
||||
export const chatModel: ProviderPackage.Definition<Settings, OpenResponsesProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => fromSettings(settings).chat(modelID)
|
||||
|
||||
export const messagesModel: ProviderPackage.Definition<Settings, MessagesOptionsInput>["model"] = (modelID, settings) =>
|
||||
fromSettings(settings).messages(modelID)
|
||||
|
||||
function fromSettings(settings: Settings) {
|
||||
return configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
headers: settings.headers,
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
providerOptions: settings.providerOptions,
|
||||
})
|
||||
}
|
||||
|
||||
export * as Meta from "./meta.js"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { chatModel as model } from "../meta.js"
|
||||
export type { Settings } from "../meta.js"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { messagesModel as model } from "../meta.js"
|
||||
export type { Settings } from "../meta.js"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { model } from "../meta.js"
|
||||
export type { Settings } from "../meta.js"
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { AnthropicMessages } from "../protocols/anthropic-messages.js"
|
||||
import { OpenAIChat } from "../protocols/openai-chat.js"
|
||||
import { OpenResponses } from "../protocols/open-responses.js"
|
||||
import { ProviderShared } from "../protocols/shared.js"
|
||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
|
||||
import { Route, type RouteDefaultsInput } from "../route/client.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { ProviderID, type LLMRequest, type ModelID } from "../schema/index.js"
|
||||
|
||||
export const id = ProviderID.make("minimax")
|
||||
|
||||
export type MessagesOptionsInput = {
|
||||
/** M3 defaults to disabled; M2.x always thinks. */
|
||||
readonly thinking?: { readonly type: "adaptive" | "disabled" }
|
||||
readonly metadata?: AnthropicMessages.OptionsInput["metadata"]
|
||||
}
|
||||
|
||||
export type ChatOptionsInput = {
|
||||
/** M3 defaults to adaptive; M2.x always thinks. */
|
||||
readonly thinking?: { readonly type: "adaptive" | "disabled" | (string & {}) }
|
||||
/** Separates reasoning from text. Defaults to true. */
|
||||
readonly reasoningSplit?: boolean
|
||||
}
|
||||
|
||||
export type ResponsesOptionsInput = {
|
||||
/** M3 defaults to none. Other supported values enable thinking without changing its depth. */
|
||||
readonly reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | (string & {})
|
||||
}
|
||||
|
||||
export type ProviderOptionsInput = MessagesOptionsInput | ChatOptionsInput | ResponsesOptionsInput
|
||||
|
||||
export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
/** Overrides the selected API's base URL, including its version prefix. */
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: ProviderOptionsInput
|
||||
}
|
||||
|
||||
export interface Settings<Options = MessagesOptionsInput> extends ProviderPackage.Settings {
|
||||
readonly apiKey?: string
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: Options
|
||||
}
|
||||
|
||||
const ChatOptions = Schema.Struct({
|
||||
thinking: Schema.optional(Schema.Struct({ type: Schema.String })),
|
||||
reasoningSplit: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
|
||||
const chatProtocol = Protocol.make({
|
||||
id: "minimax-chat",
|
||||
body: {
|
||||
schema: Schema.Struct({
|
||||
...OpenAIChat.bodyFields,
|
||||
thinking: ChatOptions.fields.thinking,
|
||||
reasoning_split: Schema.Boolean,
|
||||
}),
|
||||
from: Effect.fn("MiniMax.chatFromRequest")(function* (request: LLMRequest) {
|
||||
const options = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(ChatOptions))(
|
||||
request.providerOptions ?? {},
|
||||
)
|
||||
return {
|
||||
...(yield* OpenAIChat.protocol.body.from(request)),
|
||||
thinking: options.thinking,
|
||||
// MiniMax otherwise embeds <think> tags in ordinary assistant text.
|
||||
reasoning_split: options.reasoningSplit ?? true,
|
||||
}
|
||||
}),
|
||||
},
|
||||
stream: OpenAIChat.protocol.stream,
|
||||
})
|
||||
|
||||
const messagesRoute = Route.make({
|
||||
id: "minimax-messages",
|
||||
provider: id,
|
||||
providerMetadataKey: "minimax",
|
||||
protocol: AnthropicMessages.protocol,
|
||||
endpoint: Endpoint.path("/messages", { baseURL: "https://api.minimax.io/anthropic/v1" }),
|
||||
framing: AnthropicMessages.framing,
|
||||
headers: () => ({ "anthropic-version": "2023-06-01" }),
|
||||
})
|
||||
|
||||
const chatRoute = Route.make({
|
||||
id: "minimax-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "minimax",
|
||||
protocol: chatProtocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL: "https://api.minimax.io/v1" }),
|
||||
framing: OpenAIChat.framing,
|
||||
})
|
||||
|
||||
const responsesRoute = Route.make({
|
||||
id: "minimax-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "minimax",
|
||||
protocol: OpenResponses.protocol,
|
||||
endpoint: Endpoint.path("/responses", { baseURL: "https://api.minimax.io/v1" }),
|
||||
framing: Framing.sse,
|
||||
})
|
||||
|
||||
export const routes = [messagesRoute, chatRoute, responsesRoute]
|
||||
|
||||
export const configure = (input: Config = {}) => {
|
||||
const { apiKey: _apiKey, auth: _auth, baseURL, ...rest } = input
|
||||
const defaults = {
|
||||
...rest,
|
||||
endpoint: baseURL === undefined ? undefined : { baseURL },
|
||||
auth: AuthOptions.bearer(input, "MINIMAX_API_KEY"),
|
||||
}
|
||||
const messages = (modelID: string | ModelID) =>
|
||||
messagesRoute.with(defaults).model<MessagesOptionsInput>({ id: modelID })
|
||||
const chat = (modelID: string | ModelID) =>
|
||||
chatRoute.with(defaults).model<ChatOptionsInput>({
|
||||
id: modelID,
|
||||
compatibility: { supportsStore: false, supportsStrictMode: false },
|
||||
})
|
||||
const responses = (modelID: string | ModelID) =>
|
||||
responsesRoute.with(defaults).model<ResponsesOptionsInput>({ id: modelID })
|
||||
return { id, model: messages, messages, chat, responses, configure }
|
||||
}
|
||||
|
||||
export const provider = configure()
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings<MessagesOptionsInput>, MessagesOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) =>
|
||||
configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
headers: settings.headers,
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
providerOptions: settings.providerOptions,
|
||||
}).model(modelID)
|
||||
|
||||
export const messages = provider.messages
|
||||
export const chat = provider.chat
|
||||
export const responses = provider.responses
|
||||
|
||||
export * as MiniMax from "./minimax.js"
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { ProviderPackage } from "../../provider-package.js"
|
||||
import { MiniMax } from "../minimax.js"
|
||||
|
||||
export type Settings = MiniMax.Settings<MiniMax.ChatOptionsInput>
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, MiniMax.ChatOptionsInput>["model"] = (modelID, settings) =>
|
||||
MiniMax.configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
headers: settings.headers,
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
providerOptions: settings.providerOptions,
|
||||
}).chat(modelID)
|
||||
@@ -0,0 +1 @@
|
||||
export { model, type Settings, type MessagesOptionsInput } from "../minimax.js"
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ProviderPackage } from "../../provider-package.js"
|
||||
import { MiniMax } from "../minimax.js"
|
||||
|
||||
export type Settings = MiniMax.Settings<MiniMax.ResponsesOptionsInput>
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, MiniMax.ResponsesOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) =>
|
||||
MiniMax.configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
headers: settings.headers,
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
providerOptions: settings.providerOptions,
|
||||
}).responses(modelID)
|
||||
@@ -20,7 +20,7 @@ export interface WebSocketChannelExchange {
|
||||
readonly connect: {
|
||||
readonly url: string
|
||||
readonly headers: Headers.Headers
|
||||
/** Provider-safe connection age after which Core should rotate before sending. */
|
||||
/** Provider-safe connection age after which the channel executor should reconnect before sending. */
|
||||
readonly rotateAfterMs?: number
|
||||
}
|
||||
readonly fallback: () => Stream.Stream<string, AIError>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import { ModelID, ProviderID, RouteID } from "./ids.js"
|
||||
|
||||
export const ProviderFailureClassification = Schema.Literals(["context-overflow", "payload-too-large"])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import { LLM } from "@opencode-ai/schema/llm"
|
||||
import { LLM } from "@opencode/schema/llm"
|
||||
import { ContentBlockID, ToolCallID } from "./ids.js"
|
||||
import {
|
||||
Message,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import {
|
||||
CacheHint,
|
||||
CachePolicy,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, JsonSchema, Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import type {
|
||||
ToolCallPart,
|
||||
ToolDefinition as ToolDefinitionClass,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { AIError, ImageInput, LanguageModel, LLM, LLMClient, Provider } from "@opencode-ai/ai"
|
||||
import { Route, Protocol, WebSocketTransport } from "@opencode-ai/ai/route"
|
||||
import { Provider as ProviderSubpath } from "@opencode-ai/ai/provider"
|
||||
import { AIError, ImageInput, LanguageModel, LLM, LLMClient, Provider } from "@opencode/ai"
|
||||
import { Route, Protocol, WebSocketTransport } from "@opencode/ai/route"
|
||||
import { Provider as ProviderSubpath } from "@opencode/ai/provider"
|
||||
import {
|
||||
Baseten,
|
||||
CloudflareAIGateway,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
OpenAICompatible,
|
||||
OpenRouter,
|
||||
XAI,
|
||||
} from "@opencode-ai/ai/providers"
|
||||
} from "@opencode/ai/providers"
|
||||
import {
|
||||
OpenAIChat,
|
||||
OpenAICompatibleChat,
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
OpenAIResponses,
|
||||
OpenResponses,
|
||||
OpenResponsesChannel,
|
||||
} from "@opencode-ai/ai/protocols"
|
||||
import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages"
|
||||
import { TestLLM } from "@opencode-ai/ai/testing"
|
||||
} from "@opencode/ai/protocols"
|
||||
import * as AnthropicMessages from "@opencode/ai/protocols/anthropic-messages"
|
||||
import { TestLLM } from "@opencode/ai/testing"
|
||||
|
||||
describe("public exports", () => {
|
||||
test("root exposes app-facing runtime APIs", () => {
|
||||
@@ -46,7 +46,7 @@ describe("public exports", () => {
|
||||
})
|
||||
|
||||
test("provider barrels expose user-facing facades", async () => {
|
||||
const { OpenAICompatibleResponses } = await import("@opencode-ai/ai/providers")
|
||||
const { OpenAICompatibleResponses } = await import("@opencode/ai/providers")
|
||||
|
||||
expect(OpenAI.model).toBeFunction()
|
||||
expect(OpenAI.provider.responses).toBe(OpenAI.responses)
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "muse-spark-1.3",
|
||||
"tags": [
|
||||
"prefix:meta-chat",
|
||||
"provider:meta",
|
||||
"protocol:openai-chat",
|
||||
"tool",
|
||||
"tool-loop",
|
||||
"reasoning",
|
||||
"usage",
|
||||
"effort:low"
|
||||
],
|
||||
"name": "meta-chat/continues-a-generated-tool-call",
|
||||
"recordedAt": "2026-09-07T16:54:19.772Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.meta.ai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":\"Look up the current weather in Paris using lookup_weather before answering. After receiving the result, report Paris's weather in one short sentence.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":\"auto\",\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning_effort\":\"low\",\"max_completion_tokens\":1024}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-01a07cca-c092-7423-880d-204c05dc6645\",\"choices\":[{\"delta\":{\"content\":\"I'll look up the current weather in Paris now.\",\"role\":\"assistant\"},\"finish_reason\":null,\"index\":0}],\"created\":1788800057,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-01a07cca-c092-7423-880d-204c05dc6645\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_01a07ccac34671129bd9ef9a66fd3266\",\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"arguments\":\"\"}}]},\"finish_reason\":null,\"index\":0}],\"created\":1788800057,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-01a07cca-c092-7423-880d-204c05dc6645\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},\"finish_reason\":null,\"index\":0}],\"created\":1788800057,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-01a07cca-c092-7423-880d-204c05dc6645\",\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\",\"index\":0}],\"created\":1788800057,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":139,\"prompt_tokens\":570,\"total_tokens\":709,\"completion_tokens_details\":{\"reasoning_tokens\":70},\"prompt_tokens_details\":{\"cached_tokens\":497}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.meta.ai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":\"Look up the current weather in Paris using lookup_weather before answering. After receiving the result, report Paris's weather in one short sentence.\"},{\"role\":\"assistant\",\"content\":\"I'll look up the current weather in Paris now.\",\"tool_calls\":[{\"id\":\"call_01a07ccac34671129bd9ef9a66fd3266\",\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"call_01a07ccac34671129bd9ef9a66fd3266\",\"content\":\"{\\\"condition\\\":\\\"sunny\\\",\\\"temperature\\\":\\\"18C\\\"}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":\"auto\",\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning_effort\":\"low\",\"max_completion_tokens\":1024}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-01a07cca-c622-77e3-8e2c-9c52bac9acb5\",\"choices\":[{\"delta\":{\"content\":\"Paris is currently sunny with a\",\"role\":\"assistant\"},\"finish_reason\":null,\"index\":0}],\"created\":1788800058,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-01a07cca-c622-77e3-8e2c-9c52bac9acb5\",\"choices\":[{\"delta\":{\"content\":\" temperature of 18°C\"},\"finish_reason\":null,\"index\":0}],\"created\":1788800058,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-01a07cca-c622-77e3-8e2c-9c52bac9acb5\",\"choices\":[{\"delta\":{\"content\":\".\"},\"finish_reason\":null,\"index\":0}],\"created\":1788800058,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-01a07cca-c622-77e3-8e2c-9c52bac9acb5\",\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\",\"index\":0}],\"created\":1788800058,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":91,\"prompt_tokens\":666,\"total_tokens\":757,\"completion_tokens_details\":{\"reasoning_tokens\":69},\"prompt_tokens_details\":{\"cached_tokens\":497}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "muse-spark-1.3",
|
||||
"tags": [
|
||||
"prefix:meta-chat",
|
||||
"provider:meta",
|
||||
"protocol:openai-chat",
|
||||
"text",
|
||||
"reasoning",
|
||||
"usage",
|
||||
"effort:default"
|
||||
],
|
||||
"name": "meta-chat/streams-text-with-default-reasoning",
|
||||
"recordedAt": "2026-09-07T16:55:12.540Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.meta.ai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":1024}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-01a07ccb-8ae3-72b3-b321-b5163dda0714\",\"choices\":[{\"delta\":{\"content\":\"37887\",\"role\":\"assistant\"},\"finish_reason\":null,\"index\":0}],\"created\":1788800109,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-01a07ccb-8ae3-72b3-b321-b5163dda0714\",\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\",\"index\":0}],\"created\":1788800109,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":363,\"prompt_tokens\":23,\"total_tokens\":386,\"completion_tokens_details\":{\"reasoning_tokens\":351},\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "muse-spark-1.3",
|
||||
"tags": ["prefix:meta-chat", "provider:meta", "protocol:openai-chat", "text", "reasoning", "usage", "effort:high"],
|
||||
"name": "meta-chat/streams-text-with-high-reasoning",
|
||||
"recordedAt": "2026-09-07T16:53:30.276Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.meta.ai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning_effort\":\"high\",\"max_completion_tokens\":1024}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-01a07cc9-fe49-7773-b7f3-0be27454d3c3\",\"choices\":[{\"delta\":{\"content\":\"37887\",\"role\":\"assistant\"},\"finish_reason\":null,\"index\":0}],\"created\":1788800007,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-01a07cc9-fe49-7773-b7f3-0be27454d3c3\",\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\",\"index\":0}],\"created\":1788800007,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":324,\"prompt_tokens\":23,\"total_tokens\":347,\"completion_tokens_details\":{\"reasoning_tokens\":312},\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "muse-spark-1.3",
|
||||
"tags": ["prefix:meta-chat", "provider:meta", "protocol:openai-chat", "text", "reasoning", "usage", "effort:low"],
|
||||
"name": "meta-chat/streams-text-with-low-reasoning",
|
||||
"recordedAt": "2026-09-07T16:53:23.751Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.meta.ai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning_effort\":\"low\",\"max_completion_tokens\":1024}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-01a07cc9-eb39-7ef3-b886-5d4215128b1d\",\"choices\":[{\"delta\":{\"content\":\"37887\",\"role\":\"assistant\"},\"finish_reason\":null,\"index\":0}],\"created\":1788800002,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-01a07cc9-eb39-7ef3-b886-5d4215128b1d\",\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\",\"index\":0}],\"created\":1788800002,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":157,\"prompt_tokens\":23,\"total_tokens\":180,\"completion_tokens_details\":{\"reasoning_tokens\":145},\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "muse-spark-1.3",
|
||||
"tags": ["prefix:meta-chat", "provider:meta", "protocol:openai-chat", "text", "reasoning", "usage", "effort:max"],
|
||||
"name": "meta-chat/streams-text-with-max-reasoning",
|
||||
"recordedAt": "2026-09-07T16:53:33.749Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.meta.ai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning_effort\":\"max\",\"max_completion_tokens\":1024}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-01a07cca-10dd-7793-999d-e037320a91de\",\"choices\":[{\"delta\":{\"content\":\"37887\",\"role\":\"assistant\"},\"finish_reason\":null,\"index\":0}],\"created\":1788800012,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-01a07cca-10dd-7793-999d-e037320a91de\",\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\",\"index\":0}],\"created\":1788800012,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":195,\"prompt_tokens\":23,\"total_tokens\":218,\"completion_tokens_details\":{\"reasoning_tokens\":183},\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "muse-spark-1.3",
|
||||
"tags": [
|
||||
"prefix:meta-chat",
|
||||
"provider:meta",
|
||||
"protocol:openai-chat",
|
||||
"text",
|
||||
"reasoning",
|
||||
"usage",
|
||||
"effort:medium"
|
||||
],
|
||||
"name": "meta-chat/streams-text-with-medium-reasoning",
|
||||
"recordedAt": "2026-09-07T16:53:27.292Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.meta.ai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning_effort\":\"medium\",\"max_completion_tokens\":1024}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-01a07cc9-f131-7fb0-8895-9f0c3e79220c\",\"choices\":[{\"delta\":{\"content\":\"37887\",\"role\":\"assistant\"},\"finish_reason\":null,\"index\":0}],\"created\":1788800004,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-01a07cc9-f131-7fb0-8895-9f0c3e79220c\",\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\",\"index\":0}],\"created\":1788800004,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":273,\"prompt_tokens\":23,\"total_tokens\":296,\"completion_tokens_details\":{\"reasoning_tokens\":261},\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "muse-spark-1.3",
|
||||
"tags": [
|
||||
"prefix:meta-chat",
|
||||
"provider:meta",
|
||||
"protocol:openai-chat",
|
||||
"text",
|
||||
"reasoning",
|
||||
"usage",
|
||||
"effort:minimal"
|
||||
],
|
||||
"name": "meta-chat/streams-text-with-minimal-reasoning",
|
||||
"recordedAt": "2026-09-07T16:53:22.460Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.meta.ai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning_effort\":\"minimal\",\"max_completion_tokens\":1024}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-01a07cc9-e817-7bb1-8dc5-5197d9a4c73e\",\"choices\":[{\"delta\":{\"content\":\"37887\",\"role\":\"assistant\"},\"finish_reason\":null,\"index\":0}],\"created\":1788800002,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-01a07cc9-e817-7bb1-8dc5-5197d9a4c73e\",\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\",\"index\":0}],\"created\":1788800002,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":75,\"prompt_tokens\":23,\"total_tokens\":98,\"completion_tokens_details\":{\"reasoning_tokens\":63},\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "muse-spark-1.3",
|
||||
"tags": ["prefix:meta-chat", "provider:meta", "protocol:openai-chat", "text", "reasoning", "usage", "effort:xhigh"],
|
||||
"name": "meta-chat/streams-text-with-xhigh-reasoning",
|
||||
"recordedAt": "2026-09-07T16:53:32.103Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.meta.ai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning_effort\":\"xhigh\",\"max_completion_tokens\":1024}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-01a07cca-09de-7083-a0cc-2100de752456\",\"choices\":[{\"delta\":{\"content\":\"37887\",\"role\":\"assistant\"},\"finish_reason\":null,\"index\":0}],\"created\":1788800010,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\"}\n\ndata: {\"id\":\"chatcmpl-01a07cca-09de-7083-a0cc-2100de752456\",\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\",\"index\":0}],\"created\":1788800010,\"model\":\"muse-spark-1.3\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":253,\"prompt_tokens\":23,\"total_tokens\":276,\"completion_tokens_details\":{\"reasoning_tokens\":241},\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+29
File diff suppressed because one or more lines are too long
+29
File diff suppressed because one or more lines are too long
+55
File diff suppressed because one or more lines are too long
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "muse-spark-1.3",
|
||||
"tags": ["prefix:meta-messages", "provider:meta", "protocol:meta-messages", "tool", "tool-loop", "reasoning"],
|
||||
"name": "meta-messages/replays-encrypted-thinking-through-a-tool-loop",
|
||||
"recordedAt": "2026-09-07T17:27:03.540Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.meta.ai/v1/messages",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the current weather in Paris using lookup_weather. After receiving the result, report Paris's weather in one short sentence.\"}]}],\"tools\":[{\"name\":\"lookup_weather\",\"description\":\"Look up current weather\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"tool_choice\":{\"type\":\"auto\"},\"stream\":true,\"max_tokens\":1024,\"thinking\":{\"type\":\"adaptive\",\"display\":\"omitted\"},\"output_config\":{\"effort\":\"low\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"message\":{\"content\":[],\"id\":\"msg_6a9ef3e5a96d9a155dd4463b\",\"model\":\"muse-spark-1.3\",\"role\":\"assistant\",\"stop_reason\":null,\"stop_sequence\":null,\"type\":\"message\",\"usage\":{\"input_tokens\":0,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"data\":\"Q-PaDgGyJ4hIKk41uslnSV0PGvrTkwJ5D-t5skfjtkIt-ABXsehMcLKJJ8RJHRKW2-XhkpPrex0aOkIdqWl99vpCgtOHIFFaSc4b3oGxA8XDx4T_2aKANfYrR1DYwYzGe6ZZ-DQnU0bnpVUzCcXkghkLdyTcJr2p8cvVo1rymFpB0wZsbQRBhxCMR6PrY4i0aOTe8_waq_Po1a4l3YzKnzZIIvYP090o5kltv7MAwqChXjQeTJlKq7ECFa6HVeoTa43oiLmoD2Bzih0VfvBvQfbSh2uDypRM7-f65sp35-VuZfwsM33ZTvefzDbd8zd0D50I6wcybsj8gulDhWWpY7cxoN1Xasy-ImvisACGeppIN66maIFq5dMT2s8_CQ8a2EQ0hw9kwJaIiygZCdRofs-T1yaGWvnQxL6MpNh6SXux9T1ZfuexNrXOhz5cR3_s1euhq3-hI3GDCUDLAtkjiigSE3w6PExrwHTHnvkuAOTP0Lw7DH4NtKv-RFFeZaQOk_0Bi2EV43tR0Hq_chtEDiSpoezDjexPWfdZvhRIaO6PwyJaQ8jVOaLUr6bh6jQ3IM19lv6R7lY4Neno8fLxMwTbf7v7q02lfhOaZe3jR1fqOFUq8e4VXV76_rJBgzThqPNqO8OI_betgX5d2KMcwWAkaZjmrP3rxA7g5xp_Q7bkFiBaaTd3HbYIk_cKbtLOS6TIIphb4SX6ADA2qJ0zoo98P2NOceZODT2WrLjKCHl5dT6b9l2feH9m21pW576ULfKhVSMzuy0cmmWI7rv2P4q-2Fw2klsDVJAc6q22bFjfDgzhybKuhuM_p1SYb8aswNrgggV-cqHWDpF1FdVpL5fHMO74l1uD8Sj_9wuuD0asMQustuvsnYq2EdI0lLvONFMApWCU0s3QA8_P0Iyf0YoCZOm5QGnA7l3O5nAPkFF4kxLSsgRe9zuP6A4oKBDLN8EHwZ3pB4WZGWpUT12cJxplmT3_n8xKaMgaz13qs27uvUm3wMI1seyfpkMwUPHmt1ftpk9f_1OAg3fvQgIkWPTp6K9NzVzEry6SP7zbPfls8yn529Qrbki7ZM_oz3xEvDg367ZCe97eVnjiqRrYsYM2MmDsQq9lUwkgXb84kpK6a4pEzX9NDvhTqhw7RI6RL9E-2Ki4ciUfHv5LVTZ0rNweCo6SYXC11f8FCeObIW_Esr2mqYpORFS4SDMU-4I1HQfD9z6cPjL22APpv9a7Pp1phI8x_aQZkPU7LkyBWyVNDeusI7wq_LC0UgJ0vCAguKcAvN7gxwpCo5MEEZivRuYBldEMwRo4he2WQQke9KEokqREdlPF3Lvaav0fqPfXqhJyHAq8bYBHWdzMjDJ_dJwhdbLQ-iNLmrK5JNg2vPDglWSznBXbrSX4iyMFk1Ni2Y1SQUIOOxS_InIwCg\",\"type\":\"redacted_thinking\"},\"index\":0,\"type\":\"content_block_start\"}\n\nevent: content_block_stop\ndata: {\"index\":0,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"text\":\"\",\"type\":\"text\"},\"index\":1,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"text\":\"I'll look up the current weather in Paris.\",\"type\":\"text_delta\"},\"index\":1,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":1,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"id\":\"call_01a07ce8bb767c109c12f0a202e2ac19\",\"input\":{},\"name\":\"lookup_weather\",\"type\":\"tool_use\"},\"index\":2,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"partial_json\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"type\":\"input_json_delta\"},\"index\":2,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":2,\"type\":\"content_block_stop\"}\n\nevent: message_delta\ndata: {\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null},\"type\":\"message_delta\",\"usage\":{\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":113,\"input_tokens\":451,\"output_tokens\":155,\"output_tokens_details\":{\"thinking_tokens\":87}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.meta.ai/v1/messages",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the current weather in Paris using lookup_weather. After receiving the result, report Paris's weather in one short sentence.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"redacted_thinking\",\"data\":\"Q-PaDgGyJ4hIKk41uslnSV0PGvrTkwJ5D-t5skfjtkIt-ABXsehMcLKJJ8RJHRKW2-XhkpPrex0aOkIdqWl99vpCgtOHIFFaSc4b3oGxA8XDx4T_2aKANfYrR1DYwYzGe6ZZ-DQnU0bnpVUzCcXkghkLdyTcJr2p8cvVo1rymFpB0wZsbQRBhxCMR6PrY4i0aOTe8_waq_Po1a4l3YzKnzZIIvYP090o5kltv7MAwqChXjQeTJlKq7ECFa6HVeoTa43oiLmoD2Bzih0VfvBvQfbSh2uDypRM7-f65sp35-VuZfwsM33ZTvefzDbd8zd0D50I6wcybsj8gulDhWWpY7cxoN1Xasy-ImvisACGeppIN66maIFq5dMT2s8_CQ8a2EQ0hw9kwJaIiygZCdRofs-T1yaGWvnQxL6MpNh6SXux9T1ZfuexNrXOhz5cR3_s1euhq3-hI3GDCUDLAtkjiigSE3w6PExrwHTHnvkuAOTP0Lw7DH4NtKv-RFFeZaQOk_0Bi2EV43tR0Hq_chtEDiSpoezDjexPWfdZvhRIaO6PwyJaQ8jVOaLUr6bh6jQ3IM19lv6R7lY4Neno8fLxMwTbf7v7q02lfhOaZe3jR1fqOFUq8e4VXV76_rJBgzThqPNqO8OI_betgX5d2KMcwWAkaZjmrP3rxA7g5xp_Q7bkFiBaaTd3HbYIk_cKbtLOS6TIIphb4SX6ADA2qJ0zoo98P2NOceZODT2WrLjKCHl5dT6b9l2feH9m21pW576ULfKhVSMzuy0cmmWI7rv2P4q-2Fw2klsDVJAc6q22bFjfDgzhybKuhuM_p1SYb8aswNrgggV-cqHWDpF1FdVpL5fHMO74l1uD8Sj_9wuuD0asMQustuvsnYq2EdI0lLvONFMApWCU0s3QA8_P0Iyf0YoCZOm5QGnA7l3O5nAPkFF4kxLSsgRe9zuP6A4oKBDLN8EHwZ3pB4WZGWpUT12cJxplmT3_n8xKaMgaz13qs27uvUm3wMI1seyfpkMwUPHmt1ftpk9f_1OAg3fvQgIkWPTp6K9NzVzEry6SP7zbPfls8yn529Qrbki7ZM_oz3xEvDg367ZCe97eVnjiqRrYsYM2MmDsQq9lUwkgXb84kpK6a4pEzX9NDvhTqhw7RI6RL9E-2Ki4ciUfHv5LVTZ0rNweCo6SYXC11f8FCeObIW_Esr2mqYpORFS4SDMU-4I1HQfD9z6cPjL22APpv9a7Pp1phI8x_aQZkPU7LkyBWyVNDeusI7wq_LC0UgJ0vCAguKcAvN7gxwpCo5MEEZivRuYBldEMwRo4he2WQQke9KEokqREdlPF3Lvaav0fqPfXqhJyHAq8bYBHWdzMjDJ_dJwhdbLQ-iNLmrK5JNg2vPDglWSznBXbrSX4iyMFk1Ni2Y1SQUIOOxS_InIwCg\"},{\"type\":\"text\",\"text\":\"I'll look up the current weather in Paris.\"},{\"type\":\"tool_use\",\"id\":\"call_01a07ce8bb767c109c12f0a202e2ac19\",\"name\":\"lookup_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_01a07ce8bb767c109c12f0a202e2ac19\",\"content\":\"{\\\"condition\\\":\\\"sunny\\\"}\"}]}],\"tools\":[{\"name\":\"lookup_weather\",\"description\":\"Look up current weather\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"tool_choice\":{\"type\":\"none\"},\"stream\":true,\"max_tokens\":1024,\"thinking\":{\"type\":\"adaptive\",\"display\":\"omitted\"},\"output_config\":{\"effort\":\"low\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"message\":{\"content\":[],\"id\":\"msg_6a9ef3e77f6ef36fdd8f4925\",\"model\":\"muse-spark-1.3\",\"role\":\"assistant\",\"stop_reason\":null,\"stop_sequence\":null,\"type\":\"message\",\"usage\":{\"input_tokens\":0,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"data\":\"Q-PaDgH-GUZkiqnb_seFaP70it_GxAEIUjt2BLllpyzfckiBOUVwr72xGQsSnj0y9aOhoaQ24A_HB0MHh4XWyYziVdRgq6lDPLHkJlmMzbusZxUZUx7D7B8kD8HrBbiTeIFbFlHaM9MiZW_PpYY9fpkdc7aeuX0mORdK8XTT3JPjvW1HPp4Qqp_iEOl9G2iM2Y6xno8SqeTcbc4EQs1LePaKrq86dBPmXBjkgQbYfvIw57o0SBEyelcudtnzJnaluOyOQdV2Ytk_r_xrYSCoBwgbf2KBCOFjdeQyruuwVZZ32JJdVOpWLw8eopUwAo2xYOZP0g8S8hTmOFvDKuzXipm1OoAXwD8Swm6ED3IIo0hHA5xSfMygDCee47nd-EpShNPamkCKodfX1QvePEJsIQK2iTgkh8IGUeEEne5dxgLuXvEAbeqGDvEy6T7IoSgZnc-KPtH6SoWM_kgc_eF_oN73Nxg2prMyCqTUNg5Qs2WLPjA9wSLmmnCoiDr1bYNIuQyn6adgv0-nZZXETJoRAHJBj65Asa8kbLyCYesb192178xCj2aBSdwyj-jc0i328sa9STS7mUSI7KvOt0Yi3kllLs1aSnHW-ogsUJUM7tTf83VO9fRRU_aW4H6qr4OAr8jbBKSD3bxE0AekDd24ZS-8YIjiP1tMOAPZ2JGQrNqbxnaeqhCZzD2nl-E8TMXaAJIk4L3oZtV80xJiUW7mLEf_jQBPAWhph4ujbkDaufGvNrl4FXyBJ-XpKMIA5z0jAYU2b3ul5-Qn5Km7Oc8fPu1M_0XGAOKG-pF9ppr4y-an4B4mKDYoAiSHp3cjv-fW57D87wBfQPaJRjOzWhzqENO-5MlB0CEsnDtrLveE2ui8wSECszParUNk5SEaeSYvrXLY0QkaE7CTK9ljwTKjJM34r1a8o16KFxGuzMrloNlxIYIaO_suM9CDuf8wNwe8KkAV2Kt28g9LwkL5b3mtI7ZSjTdHYOOhFFMzs-sjXboQoGAJGOhMnCQNc3E5-goSzvOzEIXehSTs8Mzyq8d3C2k5F9PSgdO6ZouHVURDgbEXTqWqoqjQ8huk5AbrUX16QwhJambd6P-3i--Idz9CbIVuWIqiMm80TvYlMttobYxTyFDIJrKJTrVA0PkAOFvOMfYt27d7z2ZMrTf6Vtm_DNniuhVbvVU3ZcWbb4LO-XkwH3bfHB-sckpwURP34KzOMvogaqWaMAZlabdd-lQKCc7tMhmo9BrYwHHvLxAELD2qQnyym43Yo1iJNAyrqoUAFk6K029fDon7h9ybmKZdBxRtS3arUesY_91Xep0tA0Y1UYUKo8zKjK3Op_DbVyC5OoR8rxpumKiqk2PtG7RPf-WjEXGkleGqZtxix7ILSkQXxW6McY1r_6LAh2Nv0nUzqS019tTb7mx2OLyt2g\",\"type\":\"redacted_thinking\"},\"index\":0,\"type\":\"content_block_start\"}\n\nevent: content_block_stop\ndata: {\"index\":0,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"text\":\"\",\"type\":\"text\"},\"index\":1,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"text\":\"It's sunny in Paris.\",\"type\":\"text_delta\"},\"index\":1,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":1,\"type\":\"content_block_stop\"}\n\nevent: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"type\":\"message_delta\",\"usage\":{\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"input_tokens\":210,\"output_tokens\":50,\"output_tokens_details\":{\"thinking_tokens\":35}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "muse-spark-1.3",
|
||||
"tags": ["prefix:meta-messages", "provider:meta", "protocol:meta-messages", "text", "reasoning", "adaptive"],
|
||||
"name": "meta-messages/streams-text-with-adaptive-thinking",
|
||||
"recordedAt": "2026-09-07T17:26:18.565Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.meta.ai/v1/messages",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":2048,\"thinking\":{\"type\":\"adaptive\",\"display\":\"omitted\"},\"output_config\":{\"effort\":\"low\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"message\":{\"content\":[],\"id\":\"msg_6a9ef3b8e46a777cfdc94a93\",\"model\":\"muse-spark-1.3\",\"role\":\"assistant\",\"stop_reason\":null,\"stop_sequence\":null,\"type\":\"message\",\"usage\":{\"input_tokens\":0,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"data\":\"Q-PaDgFtwwNGqSqKxPk9Zj-j9xogYigpDsDHrLKPV-DxmXcbhtXDPrGPuj5qB4l1FslhuEQFCA8zIRyNGaCmzM0JG4GOZe3LpFYM2iegBbi24l4V7souryAwUs_7PrInxObuguMwh2S66ML6PwlfVRVTAwPsgDSCl6Y9tkoDHr2KBRY2fzJhmjkotuFRK2LYvNp9pKi89JxnoIKK8B_UJc_B2r4vU_Txq9_rWTmo2eXFoGOSMZNaHpVrvACRQKC1jc6-8Pj5JkJN11_Cs2fHqlaCwOoPDVJxHXUXP1AMHU1mG0jR4qf9wUaz8kMmyCBBijlfQJ4G7nsQs7LJO9gS2ropmynDoO5IEIAFf7ekxLw54Fx3JigC0_qz4Jx4a04Ey8JBzWRlZNdqggpq2_I_VoT3S0zhCYDp8ZAJQs2JqvvF-ZmLcMPH1PmpkgRySqXl5TDxERIpdL3aOUzTl-PTHVMIGEcSAz84fFwU4p1kS5llGEGC6jHlClscEEto1Y-qthL6BoeDALzk7GHEMVQD0bfC4E5yGCh0_xlWqsG3XPj0wpYKBc0hfh7vjO1NpzPxCtaDjnRwR6TzSo6x_HrEL3VELArHhpYAXl30fNO-EepduZ3Hd_kWqWj_GhdIM4dRh2zoytbhIesVD7OvBqU47urKFnpXw4kswJ-NoVVNk00ysMMHwobT9BdJL9UCoXlNzyErPe8LkA9f0VrKkuLCg5FDr_DCEYXHXr7fdRmC3JTKuzvxffvPYppUt3hs5gbdXWgT2iZzunt4bSfFne7BPkj9OGREyfLcukvGAh_oqPLXuu0lert9wbTAqRUTyM5Y1nSjw3h2B3jv91mH78mGvhy8LeVNLkrSMxEa4GtrrUvlzkPnZayc_LE2Sr7JnQjhwylVNWvkcIAjstOZShjPKe4pUJwj-cUXmXZB7Zwr-Q9WLtprFgQoBvadd5DHD4aynsYEabKZVcOqhV8l2zyaUCGobUzdPqBC5kk5-VPmNTqm6Qs91mb1y_5ydmHjTWmSW6qtN_op-Xfwqr_hllvOjhnAvvDJWUOKH-YIgZw_k_db2mzW1hO093jSDMYv6wpiy_5XWMNJBjcH8hvR_pFjXIqScUnKPranEZvCyGfHR-UBNJ89s_JEE49chcIt8LiukBeXQ_TOe29kzWnT1tYtfGQXP8FY2Wukw5yhvP3coGp1-LPZRuw3BLP2tWmiijmyj5lMikp8MIeqOztN8zgsArZ9jq21EtDSNfpO_0C4GEOtCZQYvZHRpDeY966XDnN7Blenn_8FZ95M-M-1My8hsmF__JrnY4ZFZr2274f_FbyJaSqGj6H-YRuzawO2TVLWHxAr6v2LCiB_QX01PwUI1yaJmP0mI-Qz5zXgSHzfVzhF7l1zEnM8qIYcSXzD1Gp3rjpn2sq-rR-JLqeHq080Hl0X9c8lzozw3qCpvcTfA-4ZTdXU9ax3nt7emcPVFo9N_JBjYashC7KvjkIWjMhQoTbIRlO18gjEa-Dvtsmo4_xE1ApYRoylbhxg3_fQJjKLfCBv32V6BM8qWKINL7d1nYP7PtAKnglCdnT_dK0_pCK6ejGfmdWZgVj0DhVZUCXf2kN530Y0hoCb1jBgwJ16a5gUqJMtnWScarIGSVrsYMA_uEt7DwQzGYAGESVIgi8JVATkxoq4EbyOLCHX7MIKmAPGV2WWTjWAWLnE2ITjlXtBkUAwjq2v957BMgIqxSNxEjG75Gd0i1XjZAujCSCXMrywZ72-VdQy4cD6kzH9mv04fZ7_dqn9eKPHBY7_T7V6myRDj_ImkffXcygmd7F-_-kHMExI6ABas1WtfDjkYgbRM0jMtIHYL-W9xT8vCSLSdvHcb5Y8XSXT5GTwI7eIlBHnCvFrgunbCo9bJJlT2Gc1RcgeZ4rg3yu1MNF4Mfk6DBwDhduGh5l7nypL00B0HokLfF4H7N1oSKL9jdVE3oMyJuA9NV1SJiyqcneSVHBzqjFZ5sszFXPlBKEz31izxFToAF4tDYMR507zYLcxyLA4AmHlkmn0qeozPb5qk-qZB6yLywtewv8O1KqxieAEJnEmyiqGnoKBdkjpeYmplErSxJyzoRX4fXb9mOdb-izJgvqSQ5uLm0_EXf3OklpMoAJ2CPJ6cxpg_5bhiwbgEtafCuDyXVL9Zzi872yUb_Z-ATlqU1M2Ai67f1piHIUDzMn4kulXQhPMb_R4eznMbX_aRigQh92-t1USoFtX2vmyCUQageFcrHHK524gPQ7rkOfDvZ8GHPXi3YZFnzGkSKjQD0jumFtzDiRqYkkMECep0zgY8MjuWLMjlMuigpkzSm8GPZ2aFeIMxJCYR8iMf7fH9NyHAJnicx4wdW_UqqgY5jfVtDvPSL0SRScyyK0jNdmofDHjWp4Tehih5OOw81E3BYLCiIv2p2zTMRSTZxFyG3HI91tIEaJu_RnIMFxzCilaP1LcbMrjyFzBtLJiXXJ--eFYyIMYKgfbWIOTsjLLJGC8NS2wuzaXB-C6GLsuSEHci5a8f5KGqaXK99LvHtY4k36WhEoVUjQEGY7HNJrtdjBUlqDgnT9iO06gGd2QCGvlP4us-Z3q2Bcyhf0LzOgn_Bpjc6YBk4TNF1AMxG15qmhnsrWcGElkpk3Vh9npC8--vkvhh7pOPfTZlNjQQh900OemY9gwxHyCBbt1096g4qkYOntGJmw-wVjyWxOt9K9CaM9WxY3wFyOmwqiFL1PK4CMeKMZk54iZ1ZiyXDOvsO7qUa9XQGDy3W8ic6R3DUzdkxEEBFYHAjDy3qE\",\"type\":\"redacted_thinking\"},\"index\":0,\"type\":\"content_block_start\"}\n\nevent: content_block_stop\ndata: {\"index\":0,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"text\":\"\",\"type\":\"text\"},\"index\":1,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"text\":\"37887\",\"type\":\"text_delta\"},\"index\":1,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":1,\"type\":\"content_block_stop\"}\n\nevent: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"type\":\"message_delta\",\"usage\":{\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"input_tokens\":23,\"output_tokens\":266,\"output_tokens_details\":{\"thinking_tokens\":254}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "muse-spark-1.3",
|
||||
"tags": ["prefix:meta-messages", "provider:meta", "protocol:meta-messages", "text", "reasoning", "enabled"],
|
||||
"name": "meta-messages/streams-text-with-enabled-thinking",
|
||||
"recordedAt": "2026-09-07T17:26:20.464Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.meta.ai/v1/messages",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":2048,\"thinking\":{\"type\":\"enabled\",\"budget_tokens\":1024,\"display\":\"omitted\"},\"output_config\":{\"effort\":\"low\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"message\":{\"content\":[],\"id\":\"msg_6a9ef3bbb7e691b3ea26427b\",\"model\":\"muse-spark-1.3\",\"role\":\"assistant\",\"stop_reason\":null,\"stop_sequence\":null,\"type\":\"message\",\"usage\":{\"input_tokens\":0,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"data\":\"Q-PaDgEpowvGua1C8z2RSICyyeSLruZ1JBUhrhLhgqf_XauiaESRHyHgIPVF0Cfxp7eLB_ESNObq7vJNDLdffE-RaN5nEauXhRxH6fJErC-4H_KELZFGF-qNnGu2DWl-gEojkwwkq1HtTtT3rOEk5isGjdribnDvR7a1zkpG3I0BxE_UyseISDSbkev5tyV7DkiH4xyv2ZmwuO3HHGgmAztEek9vm0ZTElZ8PIPvHG6P38aYGTGKrTjyOhmN3LBa_NFvtQQkUMKRB3n7L_N5KB1NbYkPzLEJ6L5dL9DJZBSsZms0O62QZ2zs2oWZ-ekQWL4oIuiRozWVpmwzjtkJl9WXtyvdL-4MACucXMSN9V7hWzC50g-DgdeFPSslsA9gy619wTNPPIxPymGWeD9LYKR3iwTRLNYzuycHakNqikhiwz9MweMod5GLTdbtypZJcV7ourz58jmK4YKsqleVPVvXM6bIoNuT2yGj23-tAYxQszGXQMHxSTnaLAHl6sUdiwTqX4wbFYfX-ogkIZsrca9mv5E2gkH1wJl19GPCxFaSDCHxh3QAW0RIExKUJly9Ydpoegmojfo0F4Hjt13zDm03fLEQ0eY8GjxIIHDt_zVRQGTW8d4tgZpTt4K9H1TTefG34EerN7jl7L-WNUe_gKorN4aSYBS6S6ma4Vb91NlUQ4cEGBKVQvumYe16p9Xj6Te-tCuiBmTOCq6iDsTH-eMlip_Bc18MD2BCGr2NdF4PB2vqHGSPIvSHDRXBjoPNtm18opAsUnOBu7-tysb8-EKFd5z7JbWgegQWNs11FdX0uFtC-U9ROwbHLJ3IHk4m2UHc2USwi7v4Xe6FtnkJ0VCZTajFUTXW33lwO3UsHuqEbxVnU_Rzuj6Ryjb08bpXpJBjIEGf2wBS3_I78cuXKOChHu-evtnjezc77joRUP8_I8pWsQbQ6bHABD95p8q6ZdQpDr9XQALbVg3ZvD5_QgF59rIxkW9a2L78oQpu0O3UT7-jxNFe38-OXuEr3VTv7RzISu1zjvBZD0k9-2q-T9BAJmsK6oPajJQWKwHBYF3w6vbsQ8tRDdh2uerhlAIWU0YL92gPCmT3mR6HxSGjAx08t-gib_YrzvWn0oTfLNk_BNfAMpG7Mj2GYNbyTkd_tjNZ2fOwVKbygsoFIUEvh2B-xyHOYuhXfxzI8-Gt55FMGDLLKcHfgmOZAUz1xVkIdm6bcuRFuQppgTzYIr66GN6tsr9KxTN2GZ2NHCwexe0hSAGfKVyLr8QBxTDRlzubBqe-osgQJrJtS7uM1MuWK_8grkZrNsn6g3ami0JboEmM_Ct3gVhkXo6lhCCcjn1gnGX-HOLczT3Xty1wYkeayUpi9x326nrSmypxzbzCRS3bOaPvSoBCszri0uTfUnVQK5kTVuRD30fGxauPhA\",\"type\":\"redacted_thinking\"},\"index\":0,\"type\":\"content_block_start\"}\n\nevent: content_block_stop\ndata: {\"index\":0,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"text\":\"\",\"type\":\"text\"},\"index\":1,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"text\":\"37887\",\"type\":\"text_delta\"},\"index\":1,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":1,\"type\":\"content_block_stop\"}\n\nevent: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"type\":\"message_delta\",\"usage\":{\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"input_tokens\":23,\"output_tokens\":143,\"output_tokens_details\":{\"thinking_tokens\":131}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+56
File diff suppressed because one or more lines are too long
Vendored
+37
File diff suppressed because one or more lines are too long
+37
File diff suppressed because one or more lines are too long
+37
File diff suppressed because one or more lines are too long
+37
File diff suppressed because one or more lines are too long
Vendored
+37
File diff suppressed because one or more lines are too long
Vendored
+37
File diff suppressed because one or more lines are too long
+37
File diff suppressed because one or more lines are too long
Vendored
+56
File diff suppressed because one or more lines are too long
Vendored
+56
File diff suppressed because one or more lines are too long
+56
File diff suppressed because one or more lines are too long
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "MiniMax-M3",
|
||||
"tags": [
|
||||
"prefix:minimax-chat",
|
||||
"provider:minimax",
|
||||
"protocol:minimax-chat",
|
||||
"text",
|
||||
"usage",
|
||||
"thinking-off"
|
||||
],
|
||||
"name": "minimax-chat/m3-streams-text-with-thinking-disabled",
|
||||
"recordedAt": "2026-09-07T16:53:20.976Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.minimax.io/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":1536,\"thinking\":{\"type\":\"disabled\"},\"reasoning_split\":true}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"id\":\"d8d409a572a67df400527c6592b480df\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"}}],\"created\":1788800000,\"model\":\"MiniMax-M3\",\"object\":\"chat.completion.chunk\",\"usage\":null,\"service_tier\":\"standard\"}\n\ndata: {\"id\":\"d8d409a572a67df400527c6592b480df\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"378\",\"role\":\"assistant\"}}],\"created\":1788800000,\"model\":\"MiniMax-M3\",\"object\":\"chat.completion.chunk\",\"usage\":null,\"service_tier\":\"standard\"}\n\ndata: {\"id\":\"d8d409a572a67df400527c6592b480df\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"87\",\"role\":\"assistant\"}}],\"created\":1788800000,\"model\":\"MiniMax-M3\",\"object\":\"chat.completion.chunk\",\"usage\":null,\"service_tier\":\"standard\"}\n\ndata: {\"id\":\"d8d409a572a67df400527c6592b480df\",\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,\"delta\":{\"role\":\"assistant\"}}],\"created\":1788800000,\"model\":\"MiniMax-M3\",\"object\":\"chat.completion.chunk\",\"usage\":null,\"service_tier\":\"standard\"}\n\ndata: {\"id\":\"d8d409a572a67df400527c6592b480df\",\"choices\":[],\"created\":1788800000,\"model\":\"MiniMax-M3\",\"object\":\"chat.completion.chunk\",\"usage\":{\"total_tokens\":182,\"total_characters\":0,\"prompt_tokens\":179,\"completion_tokens\":3,\"prompt_tokens_details\":{\"cached_tokens\":128}},\"service_tier\":\"standard\",\"base_resp\":{\"status_code\":0,\"status_msg\":\"\"}}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "MiniMax-M2.7",
|
||||
"tags": [
|
||||
"prefix:minimax-messages",
|
||||
"provider:minimax",
|
||||
"protocol:anthropic-messages",
|
||||
"text",
|
||||
"usage",
|
||||
"reasoning"
|
||||
],
|
||||
"name": "minimax-messages/m2-7-streams-default-thinking",
|
||||
"recordedAt": "2026-09-07T16:53:20.721Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.minimax.io/anthropic/v1/messages",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"MiniMax-M2.7\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":1536}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"bd0c49bb4f5e6f26b77bab9b5a917484\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M2.7\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":57,\"output_tokens\":0}}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"The user\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" asks: \\\"What is 173 multiplied by 219? Reply with only the final integer.\\\" So we compute 173 * 219. Compute:\\n\\n173 * 219 = \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173 * (200 + 19) = 173*200 + 173*19 = 34600 + (173*19). 173*19 = 173*20\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" - 173 = 3460 - 173 = 3287. So total = 34600 + 3287 = 37887.\\n\\nAlternatively compute directly: 219 * \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173 = 219 * 173 = (219 * 100) + (219 * 70) + (219 * 3) = 21900 + 15330 + 657\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" = 37887.\\n\\nThus answer is 37887. We must reply with only the final integer: \\\"37887\\\". No extra text.\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"1625da2f905565076ee90b9c2919db39b1719731c0820eca2f2a1c9c28324e87\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"37887\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"input_tokens\":57,\"output_tokens\":187,\"output_tokens_details\":{\"thinking_tokens\":184}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "MiniMax-M3",
|
||||
"tags": [
|
||||
"prefix:minimax-messages",
|
||||
"provider:minimax",
|
||||
"protocol:anthropic-messages",
|
||||
"tool",
|
||||
"tool-loop",
|
||||
"reasoning",
|
||||
"continuation",
|
||||
"usage"
|
||||
],
|
||||
"name": "minimax-messages/m3-continues-a-tool-loop-with-adaptive-thinking",
|
||||
"recordedAt": "2026-09-07T16:53:25.185Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.minimax.io/anthropic/v1/messages",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the current weather in Paris using get_weather before answering. After receiving the result, report the weather in one short sentence.\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get the current weather in a city\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"tool_choice\":{\"type\":\"auto\"},\"stream\":true,\"max_tokens\":1536,\"thinking\":{\"type\":\"adaptive\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"6a532037868639a6189e7c0b9e1c9aa2\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"The user wants me\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" to look up the\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" current weather in Paris\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" using the get_\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"weather tool, then\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" report it\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" in one short sentence\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\". Let\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" me call the tool\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\".\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"f05cb5f4873950f23d194289c41d79b96ebd37c9411a45b306ed44f135a910d4\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"call_01a07cc9f2d47d83a6424ff3\",\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"input_tokens\":239,\"output_tokens\":63,\"cache_read_input_tokens\":203,\"service_tier\":\"standard\",\"output_tokens_details\":{\"thinking_tokens\":33}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.minimax.io/anthropic/v1/messages",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the current weather in Paris using get_weather before answering. After receiving the result, report the weather in one short sentence.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"thinking\",\"thinking\":\"The user wants me to look up the current weather in Paris using the get_weather tool, then report it in one short sentence. Let me call the tool.\",\"signature\":\"f05cb5f4873950f23d194289c41d79b96ebd37c9411a45b306ed44f135a910d4\"},{\"type\":\"tool_use\",\"id\":\"call_01a07cc9f2d47d83a6424ff3\",\"name\":\"get_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_01a07cc9f2d47d83a6424ff3\",\"content\":\"{\\\"condition\\\":\\\"sunny\\\",\\\"temperature\\\":\\\"18C\\\"}\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get the current weather in a city\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"tool_choice\":{\"type\":\"none\"},\"stream\":true,\"max_tokens\":1536,\"thinking\":{\"type\":\"adaptive\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"e9e7ea89f63eca1cd17a037aef28eed1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"The weather\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" in Paris is sunny\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" with\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" a\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" temperature of 18\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"°C.\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"input_tokens\":164,\"output_tokens\":16,\"cache_read_input_tokens\":128,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "MiniMax-M3",
|
||||
"tags": [
|
||||
"prefix:minimax-messages",
|
||||
"provider:minimax",
|
||||
"protocol:anthropic-messages",
|
||||
"tool",
|
||||
"thinking-off",
|
||||
"usage"
|
||||
],
|
||||
"name": "minimax-messages/m3-generates-a-named-tool-call-with-default-thinking-off",
|
||||
"recordedAt": "2026-09-07T16:53:24.366Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.minimax.io/anthropic/v1/messages",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Use get_weather to look up the current weather in Paris.\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get the current weather in a city\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"tool_choice\":{\"type\":\"tool\",\"name\":\"get_weather\"},\"stream\":true,\"max_tokens\":512}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"a60a8adb2eff7545f809b9df2689f1b8\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"I'll\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" look\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" up the current weather\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" in Paris for you\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\".\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"call_b0246853f3c4432ea455cc99\",\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"input_tokens\":401,\"output_tokens\":39,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "MiniMax-M3",
|
||||
"tags": [
|
||||
"prefix:minimax-messages",
|
||||
"provider:minimax",
|
||||
"protocol:anthropic-messages",
|
||||
"text",
|
||||
"usage",
|
||||
"reasoning"
|
||||
],
|
||||
"name": "minimax-messages/m3-streams-adaptive-thinking",
|
||||
"recordedAt": "2026-09-07T16:53:16.470Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.minimax.io/anthropic/v1/messages",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":1536,\"thinking\":{\"type\":\"adaptive\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"ece24e3da050a8b1d7e8b1ad924a9e17\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" × 219\\n\\n\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173 × 200\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" = 346\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"00\\n173 ×\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" 19 = \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173 × 20\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" - 173 =\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" 3460\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" - 173 =\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" 3287\\n\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"34600 + \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"3287 = \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"37887\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"af0089d30b3ee7aff92a96e68064f4ed9346ca97de4f579555d2b3e1e61b53ea\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"37887\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"input_tokens\":64,\"output_tokens\":54,\"cache_read_input_tokens\":128,\"service_tier\":\"standard\",\"output_tokens_details\":{\"thinking_tokens\":49}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "MiniMax-M3",
|
||||
"tags": [
|
||||
"prefix:minimax-messages",
|
||||
"provider:minimax",
|
||||
"protocol:anthropic-messages",
|
||||
"text",
|
||||
"usage",
|
||||
"thinking-off"
|
||||
],
|
||||
"name": "minimax-messages/m3-streams-text-with-thinking-disabled",
|
||||
"recordedAt": "2026-09-07T16:53:16.028Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.minimax.io/anthropic/v1/messages",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":1536,\"thinking\":{\"type\":\"disabled\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"01664c047875a0f573c966467c8cccc1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"378\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"87\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"input_tokens\":51,\"output_tokens\":3,\"cache_read_input_tokens\":128,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+56
File diff suppressed because one or more lines are too long
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "MiniMax-M3",
|
||||
"tags": [
|
||||
"prefix:minimax-responses",
|
||||
"provider:minimax",
|
||||
"protocol:open-responses",
|
||||
"text",
|
||||
"usage",
|
||||
"thinking-off"
|
||||
],
|
||||
"name": "minimax-responses/m3-streams-text-with-effort-none",
|
||||
"recordedAt": "2026-09-07T16:53:21.223Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.minimax.io/v1/responses",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"MiniMax-M3\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"reasoning\":{\"effort\":\"none\"},\"max_output_tokens\":1536,\"stream\":true}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: response.created\ndata: {\"type\":\"response.created\",\"sequence_number\":0,\"response\":{\"id\":\"40d878e9c7ecc67905d069cdf9bec7c0\",\"object\":\"response\",\"created_at\":1788800001,\"model\":\"MiniMax-M3\",\"status\":\"in_progress\",\"output\":[],\"output_text\":null,\"usage\":null,\"error\":null,\"incomplete_details\":null,\"instructions\":null,\"metadata\":{},\"tools\":null,\"tool_choice\":\"auto\",\"temperature\":1,\"top_p\":0.95,\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":{\"effort\":\"none\",\"summary\":null},\"max_output_tokens\":1536,\"parallel_tool_calls\":true,\"previous_response_id\":null,\"conversation\":null,\"store\":false,\"service_tier\":\"standard\",\"safety_identifier\":null,\"truncation\":\"disabled\"}}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"sequence_number\":1,\"response\":{\"id\":\"40d878e9c7ecc67905d069cdf9bec7c0\",\"object\":\"response\",\"created_at\":1788800001,\"model\":\"MiniMax-M3\",\"status\":\"in_progress\",\"output\":[],\"output_text\":null,\"usage\":null,\"error\":null,\"incomplete_details\":null,\"instructions\":null,\"metadata\":{},\"tools\":null,\"tool_choice\":\"auto\",\"temperature\":1,\"top_p\":0.95,\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":{\"effort\":\"none\",\"summary\":null},\"max_output_tokens\":1536,\"parallel_tool_calls\":true,\"previous_response_id\":null,\"conversation\":null,\"store\":false,\"service_tier\":\"standard\",\"safety_identifier\":null,\"truncation\":\"disabled\"}}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"sequence_number\":2,\"item\":{\"id\":\"40d878e9c7ecc67905d069cdf9bec7c0_msg\",\"type\":\"message\",\"status\":\"in_progress\",\"role\":\"assistant\",\"content\":[]},\"output_index\":0}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"sequence_number\":3,\"part\":{\"type\":\"output_text\",\"text\":\"\",\"annotations\":[]},\"output_index\":0,\"content_index\":0,\"item_id\":\"40d878e9c7ecc67905d069cdf9bec7c0_msg\"}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"sequence_number\":4,\"output_index\":0,\"content_index\":0,\"item_id\":\"40d878e9c7ecc67905d069cdf9bec7c0_msg\",\"delta\":\"378\"}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"sequence_number\":5,\"output_index\":0,\"content_index\":0,\"item_id\":\"40d878e9c7ecc67905d069cdf9bec7c0_msg\",\"delta\":\"87\"}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"sequence_number\":6,\"output_index\":0,\"content_index\":0,\"item_id\":\"40d878e9c7ecc67905d069cdf9bec7c0_msg\",\"text\":\"37887\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"sequence_number\":7,\"part\":{\"type\":\"output_text\",\"text\":\"37887\",\"annotations\":[]},\"output_index\":0,\"content_index\":0,\"item_id\":\"40d878e9c7ecc67905d069cdf9bec7c0_msg\"}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"sequence_number\":8,\"item\":{\"status\":\"completed\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"37887\",\"annotations\":[]}],\"id\":\"40d878e9c7ecc67905d069cdf9bec7c0_msg\",\"type\":\"message\"},\"output_index\":0}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"sequence_number\":9,\"response\":{\"id\":\"40d878e9c7ecc67905d069cdf9bec7c0\",\"object\":\"response\",\"created_at\":1788800001,\"model\":\"MiniMax-M3\",\"status\":\"completed\",\"output\":[{\"id\":\"40d878e9c7ecc67905d069cdf9bec7c0_msg\",\"type\":\"message\",\"status\":\"completed\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"37887\",\"annotations\":null}]}],\"output_text\":\"37887\",\"usage\":{\"input_tokens\":179,\"output_tokens\":3,\"total_tokens\":182,\"input_tokens_details\":{\"cached_tokens\":128}},\"error\":null,\"incomplete_details\":null,\"instructions\":null,\"metadata\":{},\"tools\":null,\"tool_choice\":\"auto\",\"temperature\":1,\"top_p\":0.95,\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":{\"effort\":\"none\",\"summary\":null},\"max_output_tokens\":1536,\"parallel_tool_calls\":true,\"previous_response_id\":null,\"conversation\":null,\"store\":false,\"service_tier\":\"standard\",\"safety_identifier\":null,\"truncation\":\"disabled\"}}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { LLM } from "../../src/index.js"
|
||||
import { MiniMax } from "../../src/providers.js"
|
||||
|
||||
const minimax = MiniMax.configure()
|
||||
|
||||
LLM.request({ model: minimax.model("MiniMax-M3"), providerOptions: { thinking: { type: "adaptive" } } })
|
||||
LLM.request({ model: minimax.chat("MiniMax-M3"), providerOptions: { thinking: { type: "disabled" } } })
|
||||
LLM.request({ model: minimax.chat("MiniMax-M3"), providerOptions: { reasoningSplit: false } })
|
||||
LLM.request({ model: minimax.responses("MiniMax-M3"), providerOptions: { reasoningEffort: "minimal" } })
|
||||
LLM.request({ model: minimax.responses("MiniMax-M3"), providerOptions: { reasoningEffort: "future-effort" } })
|
||||
|
||||
LLM.request({
|
||||
model: minimax.model("MiniMax-M3"),
|
||||
// @ts-expect-error MiniMax Messages has no documented effort setting.
|
||||
providerOptions: { effort: "high" },
|
||||
})
|
||||
LLM.request({
|
||||
model: minimax.chat("MiniMax-M3"),
|
||||
// @ts-expect-error Chat reasoning_split is a boolean.
|
||||
providerOptions: { reasoningSplit: "true" },
|
||||
})
|
||||
LLM.request({
|
||||
model: minimax.responses("MiniMax-M3"),
|
||||
// @ts-expect-error MiniMax Responses uses reasoning effort rather than Messages thinking.
|
||||
providerOptions: { thinking: { type: "adaptive" } },
|
||||
})
|
||||
@@ -1,42 +1,46 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { model } from "@opencode-ai/ai/providers/openai"
|
||||
import { model } from "@opencode/ai/providers/openai"
|
||||
import { LLM } from "../src/index.js"
|
||||
import { Endpoint } from "../src/route/endpoint.js"
|
||||
|
||||
describe("provider package entrypoints", () => {
|
||||
test("semantic API aliases expose the same contract", async () => {
|
||||
const modules = await Promise.all([
|
||||
import("@opencode-ai/ai/providers/openai"),
|
||||
import("@opencode-ai/ai/providers/openai/responses"),
|
||||
import("@opencode-ai/ai/providers/openai/chat"),
|
||||
import("@opencode-ai/ai/providers/anthropic"),
|
||||
import("@opencode-ai/ai/providers/anthropic-compatible"),
|
||||
import("@opencode-ai/ai/providers/openai-compatible"),
|
||||
import("@opencode-ai/ai/providers/openai-compatible/responses"),
|
||||
import("@opencode-ai/ai/providers/amazon-bedrock"),
|
||||
import("@opencode-ai/ai/providers/azure"),
|
||||
import("@opencode-ai/ai/providers/azure/responses"),
|
||||
import("@opencode-ai/ai/providers/azure/chat"),
|
||||
import("@opencode-ai/ai/providers/google"),
|
||||
import("@opencode-ai/ai/providers/google-vertex"),
|
||||
import("@opencode-ai/ai/providers/google-vertex/gemini"),
|
||||
import("@opencode-ai/ai/providers/google-vertex/chat"),
|
||||
import("@opencode-ai/ai/providers/google-vertex/responses"),
|
||||
import("@opencode-ai/ai/providers/google-vertex/messages"),
|
||||
import("@opencode-ai/ai/providers/openrouter"),
|
||||
import("@opencode-ai/ai/providers/xai"),
|
||||
import("@opencode-ai/ai/providers/amazon-bedrock/mantle"),
|
||||
import("@opencode-ai/ai/providers/amazon-bedrock/mantle/chat"),
|
||||
import("@opencode-ai/ai/providers/amazon-bedrock/mantle/responses"),
|
||||
import("@opencode-ai/ai/providers/togetherai"),
|
||||
import("@opencode-ai/ai/providers/cerebras"),
|
||||
import("@opencode-ai/ai/providers/deepinfra"),
|
||||
import("@opencode-ai/ai/providers/groq"),
|
||||
import("@opencode-ai/ai/providers/baseten"),
|
||||
import("@opencode-ai/ai/providers/deepseek"),
|
||||
import("@opencode-ai/ai/providers/fireworks"),
|
||||
import("@opencode-ai/ai/providers/cloudflare-ai-gateway"),
|
||||
import("@opencode-ai/ai/providers/cloudflare-workers-ai"),
|
||||
import("@opencode/ai/providers/openai"),
|
||||
import("@opencode/ai/providers/openai/responses"),
|
||||
import("@opencode/ai/providers/openai/chat"),
|
||||
import("@opencode/ai/providers/anthropic"),
|
||||
import("@opencode/ai/providers/anthropic-compatible"),
|
||||
import("@opencode/ai/providers/openai-compatible"),
|
||||
import("@opencode/ai/providers/openai-compatible/responses"),
|
||||
import("@opencode/ai/providers/amazon-bedrock"),
|
||||
import("@opencode/ai/providers/azure"),
|
||||
import("@opencode/ai/providers/azure/responses"),
|
||||
import("@opencode/ai/providers/azure/chat"),
|
||||
import("@opencode/ai/providers/google"),
|
||||
import("@opencode/ai/providers/google-vertex"),
|
||||
import("@opencode/ai/providers/google-vertex/gemini"),
|
||||
import("@opencode/ai/providers/google-vertex/chat"),
|
||||
import("@opencode/ai/providers/google-vertex/responses"),
|
||||
import("@opencode/ai/providers/google-vertex/messages"),
|
||||
import("@opencode/ai/providers/openrouter"),
|
||||
import("@opencode/ai/providers/xai"),
|
||||
import("@opencode/ai/providers/amazon-bedrock/mantle"),
|
||||
import("@opencode/ai/providers/amazon-bedrock/mantle/chat"),
|
||||
import("@opencode/ai/providers/amazon-bedrock/mantle/responses"),
|
||||
import("@opencode/ai/providers/togetherai"),
|
||||
import("@opencode/ai/providers/cerebras"),
|
||||
import("@opencode/ai/providers/deepinfra"),
|
||||
import("@opencode/ai/providers/groq"),
|
||||
import("@opencode/ai/providers/baseten"),
|
||||
import("@opencode/ai/providers/deepseek"),
|
||||
import("@opencode/ai/providers/fireworks"),
|
||||
import("@opencode/ai/providers/cloudflare-ai-gateway"),
|
||||
import("@opencode/ai/providers/cloudflare-workers-ai"),
|
||||
import("@opencode/ai/providers/minimax"),
|
||||
import("@opencode/ai/providers/minimax/messages"),
|
||||
import("@opencode/ai/providers/minimax/chat"),
|
||||
import("@opencode/ai/providers/minimax/responses"),
|
||||
])
|
||||
|
||||
for (const module of modules) expect(module.model).toBeFunction()
|
||||
@@ -47,8 +51,33 @@ describe("provider package entrypoints", () => {
|
||||
expect(modules[19].model).not.toBe(modules[20].model)
|
||||
})
|
||||
|
||||
test("maps MiniMax API entrypoints onto provider-owned routes", async () => {
|
||||
const modules = await Promise.all([
|
||||
import("@opencode/ai/providers/minimax"),
|
||||
import("@opencode/ai/providers/minimax/messages"),
|
||||
import("@opencode/ai/providers/minimax/chat"),
|
||||
import("@opencode/ai/providers/minimax/responses"),
|
||||
])
|
||||
expect(modules[0].model).toBe(modules[1].model)
|
||||
const settings = {
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://gateway.example/v1",
|
||||
headers: { "x-application": "opencode" },
|
||||
body: { service_tier: "priority" },
|
||||
}
|
||||
const routes = ["minimax-messages", "minimax-messages", "minimax-chat", "minimax-responses"]
|
||||
modules.forEach((module, index) => {
|
||||
const selected = module.model("MiniMax-M3", settings)
|
||||
expect(selected.provider).toBe("minimax")
|
||||
expect(selected.route.id).toBe(routes[index])
|
||||
expect(selected.route.endpoint.baseURL).toBe(settings.baseURL)
|
||||
expect(selected.route.defaults.headers).toEqual(settings.headers)
|
||||
expect(selected.route.defaults.http?.body).toEqual(settings.body)
|
||||
})
|
||||
})
|
||||
|
||||
test("maps DeepInfra package settings onto its native executable model", async () => {
|
||||
const DeepInfra = await import("@opencode-ai/ai/providers/deepinfra")
|
||||
const DeepInfra = await import("@opencode/ai/providers/deepinfra")
|
||||
const settings = {
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://provider.example.test/v1/",
|
||||
@@ -67,8 +96,8 @@ describe("provider package entrypoints", () => {
|
||||
|
||||
test("maps Cloudflare package settings onto provider-owned models", async () => {
|
||||
const modules = await Promise.all([
|
||||
import("@opencode-ai/ai/providers/cloudflare-ai-gateway"),
|
||||
import("@opencode-ai/ai/providers/cloudflare-workers-ai"),
|
||||
import("@opencode/ai/providers/cloudflare-ai-gateway"),
|
||||
import("@opencode/ai/providers/cloudflare-workers-ai"),
|
||||
])
|
||||
for (const provider of modules) {
|
||||
const selected = provider.model("provider-model", {
|
||||
@@ -87,8 +116,8 @@ describe("provider package entrypoints", () => {
|
||||
})
|
||||
|
||||
test("maps OpenRouter and xAI package settings onto executable models", async () => {
|
||||
const OpenRouter = await import("@opencode-ai/ai/providers/openrouter")
|
||||
const XAI = await import("@opencode-ai/ai/providers/xai")
|
||||
const OpenRouter = await import("@opencode/ai/providers/openrouter")
|
||||
const XAI = await import("@opencode/ai/providers/xai")
|
||||
const settings = {
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://provider.example.test/v1",
|
||||
@@ -128,7 +157,7 @@ describe("provider package entrypoints", () => {
|
||||
})
|
||||
|
||||
test("maps OpenAI-compatible Responses settings onto the executable model", async () => {
|
||||
const OpenAICompatibleResponses = await import("@opencode-ai/ai/providers/openai-compatible/responses")
|
||||
const OpenAICompatibleResponses = await import("@opencode/ai/providers/openai-compatible/responses")
|
||||
const selected = OpenAICompatibleResponses.model("custom-model", {
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://responses.example.test/v1",
|
||||
@@ -154,7 +183,7 @@ describe("provider package entrypoints", () => {
|
||||
})
|
||||
|
||||
test("maps Anthropic-compatible settings onto the executable model", async () => {
|
||||
const AnthropicCompatible = await import("@opencode-ai/ai/providers/anthropic-compatible")
|
||||
const AnthropicCompatible = await import("@opencode/ai/providers/anthropic-compatible")
|
||||
const selected = AnthropicCompatible.model("compatible-model", {
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://messages.example.test/v1",
|
||||
@@ -178,7 +207,7 @@ describe("provider package entrypoints", () => {
|
||||
})
|
||||
|
||||
test("maps Anthropic provider options onto the executable model", async () => {
|
||||
const Anthropic = await import("@opencode-ai/ai/providers/anthropic")
|
||||
const Anthropic = await import("@opencode/ai/providers/anthropic")
|
||||
const selected = Anthropic.model("claude-sonnet-4-6", {
|
||||
apiKey: "fixture",
|
||||
providerOptions: { thinking: { type: "adaptive" } },
|
||||
@@ -188,15 +217,15 @@ describe("provider package entrypoints", () => {
|
||||
})
|
||||
|
||||
test("requires an Anthropic-compatible base URL at runtime", async () => {
|
||||
const AnthropicCompatible = await import("@opencode-ai/ai/providers/anthropic-compatible")
|
||||
const AnthropicCompatible = await import("@opencode/ai/providers/anthropic-compatible")
|
||||
expect(() =>
|
||||
Reflect.apply(AnthropicCompatible.model, undefined, ["compatible-model", { apiKey: "fixture" }]),
|
||||
).toThrow("Anthropic-compatible providers require a baseURL")
|
||||
})
|
||||
|
||||
test("rejects conflicting Anthropic-compatible auth settings at runtime", async () => {
|
||||
const Anthropic = await import("@opencode-ai/ai/providers/anthropic")
|
||||
const AnthropicCompatible = await import("@opencode-ai/ai/providers/anthropic-compatible")
|
||||
const Anthropic = await import("@opencode/ai/providers/anthropic")
|
||||
const AnthropicCompatible = await import("@opencode/ai/providers/anthropic-compatible")
|
||||
expect(() =>
|
||||
Reflect.apply(AnthropicCompatible.model, undefined, [
|
||||
"compatible-model",
|
||||
@@ -226,9 +255,9 @@ describe("provider package entrypoints", () => {
|
||||
})
|
||||
|
||||
test("selects Azure API entrypoints with the same model contract", async () => {
|
||||
const Azure = await import("@opencode-ai/ai/providers/azure")
|
||||
const AzureChat = await import("@opencode-ai/ai/providers/azure/chat")
|
||||
const AzureResponses = await import("@opencode-ai/ai/providers/azure/responses")
|
||||
const Azure = await import("@opencode/ai/providers/azure")
|
||||
const AzureChat = await import("@opencode/ai/providers/azure/chat")
|
||||
const AzureResponses = await import("@opencode/ai/providers/azure/responses")
|
||||
const settings = {
|
||||
apiKey: "fixture",
|
||||
resourceName: "opencode-test",
|
||||
@@ -248,7 +277,7 @@ describe("provider package entrypoints", () => {
|
||||
})
|
||||
|
||||
test("constructs Azure deployment URLs and preserves custom gateway URLs", async () => {
|
||||
const Azure = await import("@opencode-ai/ai/providers/azure")
|
||||
const Azure = await import("@opencode/ai/providers/azure")
|
||||
const deployment = Azure.model("custom-deployment", {
|
||||
apiKey: "fixture",
|
||||
resourceName: "opencode-test",
|
||||
@@ -269,7 +298,7 @@ describe("provider package entrypoints", () => {
|
||||
})
|
||||
|
||||
test("maps Google package settings onto the Gemini model", async () => {
|
||||
const Google = await import("@opencode-ai/ai/providers/google")
|
||||
const Google = await import("@opencode/ai/providers/google")
|
||||
const selected = Google.model("gemini-2.5-flash", {
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://generativelanguage.test/v1beta",
|
||||
@@ -286,11 +315,11 @@ describe("provider package entrypoints", () => {
|
||||
})
|
||||
|
||||
test("selects Vertex entrypoints with the same model contract", async () => {
|
||||
const GoogleVertex = await import("@opencode-ai/ai/providers/google-vertex")
|
||||
const GoogleVertexGemini = await import("@opencode-ai/ai/providers/google-vertex/gemini")
|
||||
const GoogleVertexChat = await import("@opencode-ai/ai/providers/google-vertex/chat")
|
||||
const GoogleVertexResponses = await import("@opencode-ai/ai/providers/google-vertex/responses")
|
||||
const GoogleVertexMessages = await import("@opencode-ai/ai/providers/google-vertex/messages")
|
||||
const GoogleVertex = await import("@opencode/ai/providers/google-vertex")
|
||||
const GoogleVertexGemini = await import("@opencode/ai/providers/google-vertex/gemini")
|
||||
const GoogleVertexChat = await import("@opencode/ai/providers/google-vertex/chat")
|
||||
const GoogleVertexResponses = await import("@opencode/ai/providers/google-vertex/responses")
|
||||
const GoogleVertexMessages = await import("@opencode/ai/providers/google-vertex/messages")
|
||||
const gemini = GoogleVertex.model("gemini-3.5-flash", {
|
||||
apiKey: "fixture",
|
||||
headers: { "x-application": "opencode" },
|
||||
@@ -349,11 +378,11 @@ describe("provider package entrypoints", () => {
|
||||
})
|
||||
|
||||
test("rejects conflicting Vertex auth settings at runtime", async () => {
|
||||
const GoogleVertex = await import("@opencode-ai/ai/providers/google-vertex")
|
||||
const GoogleVertexChat = await import("@opencode-ai/ai/providers/google-vertex/chat")
|
||||
const GoogleVertexMessages = await import("@opencode-ai/ai/providers/google-vertex/messages")
|
||||
const GoogleVertexResponses = await import("@opencode-ai/ai/providers/google-vertex/responses")
|
||||
const Providers = await import("@opencode-ai/ai/providers")
|
||||
const GoogleVertex = await import("@opencode/ai/providers/google-vertex")
|
||||
const GoogleVertexChat = await import("@opencode/ai/providers/google-vertex/chat")
|
||||
const GoogleVertexMessages = await import("@opencode/ai/providers/google-vertex/messages")
|
||||
const GoogleVertexResponses = await import("@opencode/ai/providers/google-vertex/responses")
|
||||
const Providers = await import("@opencode/ai/providers")
|
||||
expect(() =>
|
||||
Reflect.apply(GoogleVertex.model, undefined, [
|
||||
"gemini-3.5-flash",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { configure } from "@opencode-ai/ai/providers/groq"
|
||||
import { configure } from "@opencode/ai/providers/groq"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMEvent, LLMRequest, LLMResponse, Message, ToolChoice, ToolDefinition } from "../../src/index.js"
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { Image, ImageClient, ImageInput, LLM, LLMEvent, LLMRequest, Message, ToolDefinition } from "../../src/index.js"
|
||||
import { Meta } from "../../src/providers/meta.js"
|
||||
import { MetaMessages } from "../../src/protocols/meta-messages.js"
|
||||
import { AnthropicMessages } from "../../src/protocols/anthropic-messages.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
import { dynamicResponse, fixedResponse } from "../lib/http.js"
|
||||
import { sseEvents } from "../lib/sse.js"
|
||||
|
||||
it.effect("Meta selects Messages and lowers native search alongside ordinary functions", () =>
|
||||
Effect.gen(function* () {
|
||||
const model = Meta.messagesModel("muse-spark-1.3", { apiKey: "fixture", baseURL: "https://gateway.example/v1" })
|
||||
expect(model.route.endpoint).toMatchObject({ baseURL: "https://gateway.example/v1", path: "/messages" })
|
||||
expect(MetaMessages.protocol.stream).toBe(AnthropicMessages.protocol.stream)
|
||||
const compiled = yield* compileRequest(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Search",
|
||||
tools: [
|
||||
Meta.webSearch({ userLocation: { country: "US" } }),
|
||||
ToolDefinition.make({ name: "lookup", description: "Lookup", inputSchema: { type: "object" } }),
|
||||
],
|
||||
providerOptions: { effort: "low" },
|
||||
generation: { maxTokens: 1024 },
|
||||
}),
|
||||
)
|
||||
expect(compiled.body).toMatchObject({
|
||||
max_tokens: 1024,
|
||||
thinking: { type: "adaptive", display: "omitted" },
|
||||
output_config: { effort: "low" },
|
||||
tools: [
|
||||
{ type: "web_search", name: "web_search", user_location: { type: "approximate", country: "US" } },
|
||||
{ name: "lookup", description: "Lookup", input_schema: { type: "object" } },
|
||||
],
|
||||
})
|
||||
const entrypoint = yield* Effect.promise(() => import("@opencode/ai/providers/meta/messages"))
|
||||
expect(entrypoint.model("muse-spark-1.3", {}).route.id).toBe("meta-messages")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("Meta rejects unsupported native tools instead of sending them as local functions", () =>
|
||||
Effect.gen(function* () {
|
||||
for (const input of [
|
||||
{
|
||||
model: Meta.responses("muse-spark-1.3"),
|
||||
tool: ToolDefinition.make({
|
||||
name: "foreign",
|
||||
description: "Foreign tool",
|
||||
inputSchema: {},
|
||||
native: { other: { type: "web_search" } },
|
||||
}),
|
||||
},
|
||||
{ model: Meta.messages("muse-spark-1.3"), tool: Meta.imageGeneration() },
|
||||
{ model: Meta.messages("muse-spark-1.3"), tool: Meta.webSearch({ searchContextSize: "low" }) },
|
||||
]) {
|
||||
const error = yield* compileRequest(
|
||||
LLM.request({ model: input.model, prompt: "Hello", tools: [input.tool] }),
|
||||
).pipe(Effect.flip)
|
||||
expect(error.reason._tag).toBe("InvalidRequest")
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("Meta Images preserves request overlays, bearer auth, JSON edit inputs and URL output formats", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* Image.generate({
|
||||
model: Meta.configure({
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://gateway.example/v1",
|
||||
headers: { "x-client": "test" },
|
||||
}).image("muse-image-1.0"),
|
||||
prompt: "Edit",
|
||||
images: [ImageInput.bytes(Uint8Array.from([1, 2, 3]), "image/png")],
|
||||
options: {
|
||||
outputFormat: "webp",
|
||||
responseFormat: "url",
|
||||
reasoningStrength: "low",
|
||||
toolEnablement: { enable_web_search: false },
|
||||
output_format: "png",
|
||||
},
|
||||
http: { body: { output_format: "jpeg", future_option: true }, query: { trace: "1" } },
|
||||
})
|
||||
expect(response.image?.mediaType).toBe("image/jpeg")
|
||||
expect(response.image?.data).toBe("https://images.example/result.jpg")
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
ImageClient.layer.pipe(
|
||||
Layer.provide(
|
||||
dynamicResponse((input) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
||||
expect(request.url).toBe("https://gateway.example/v1/images/edits?trace=1")
|
||||
expect(input.request.headers.authorization).toBe("Bearer fixture")
|
||||
expect(input.request.headers["x-client"]).toBe("test")
|
||||
expect(JSON.parse(input.text)).toEqual({
|
||||
model: "muse-image-1.0",
|
||||
prompt: "Edit",
|
||||
images: [{ image_url: "data:image/png;base64,AQID" }],
|
||||
output_format: "jpeg",
|
||||
response_format: "url",
|
||||
reasoning_strength: "low",
|
||||
tool_enablement: { enable_web_search: false },
|
||||
future_option: true,
|
||||
})
|
||||
return input.respond(JSON.stringify({ data: [{ url: "https://images.example/result.jpg" }] }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("Meta Images validates the final output format before sending the request", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* Image.generate({
|
||||
model: Meta.configure({ apiKey: "fixture" }).image("muse-image-1.0"),
|
||||
prompt: "Draw",
|
||||
options: { outputFormat: "png" },
|
||||
http: { body: { output_format: 42 } },
|
||||
}).pipe(Effect.flip)
|
||||
expect(error.reason._tag).toBe("InvalidRequest")
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
ImageClient.layer.pipe(
|
||||
Layer.provide(dynamicResponse(() => Effect.die("Invalid image requests must not reach HTTP"))),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
for (const streamed of [false, true]) {
|
||||
it.effect(
|
||||
`Meta recovers terminal image output ${streamed ? "without duplicating streamed items" : "with its signed replay handle"}`,
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const item = { type: "image_generation_call", id: "ig_signed", status: "completed", result: "iVBORw0KGgo=" }
|
||||
const request = LLM.request({
|
||||
model: Meta.configure({ apiKey: "fixture" }).responses("muse-image-1.0"),
|
||||
prompt: "Draw",
|
||||
})
|
||||
const response = yield* LLM.generate(request).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
sseEvents(
|
||||
{ type: "response.created", response: { id: "resp_image" } },
|
||||
...(streamed
|
||||
? [
|
||||
{ type: "response.output_item.added", output_index: 0, item },
|
||||
{ type: "response.output_item.done", output_index: 0, item },
|
||||
]
|
||||
: []),
|
||||
{ type: "response.completed", response: { id: "resp_image", output: [item] } },
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
expect(response.events.filter(LLMEvent.is.toolResult)).toHaveLength(1)
|
||||
expect(response.events.filter(LLMEvent.is.finish)).toHaveLength(1)
|
||||
expect(response.events.filter(LLMEvent.is.toolResult)[0]?.result).toEqual({
|
||||
type: "content",
|
||||
value: [{ type: "file", mime: "image/png", uri: "data:image/png;base64,iVBORw0KGgo=" }],
|
||||
})
|
||||
const replay = yield* compileRequest(
|
||||
LLMRequest.update(request, { messages: [...request.messages, response.message, Message.user("Edit")] }),
|
||||
)
|
||||
expect(replay.body.input).toContainEqual({
|
||||
type: "image_generation_call",
|
||||
id: "ig_signed",
|
||||
status: "completed",
|
||||
result: null,
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
it.effect("Meta rejects malformed image data from terminal-only Responses output", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLM.generate(
|
||||
LLM.request({ model: Meta.configure({ apiKey: "fixture" }).responses("muse-image-1.0"), prompt: "Draw" }),
|
||||
).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
sseEvents({
|
||||
type: "response.completed",
|
||||
response: {
|
||||
id: "resp_invalid",
|
||||
output: [{ type: "image_generation_call", id: "ig_invalid", result: "!not-base64!" }],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.flip,
|
||||
)
|
||||
expect(error.reason._tag).toBe("InvalidProviderOutput")
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,118 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Image, ImageInput, LLM, LLMEvent, LLMRequest, Message } from "../../src/index.js"
|
||||
import { Meta } from "../../src/providers/meta.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
import { recordedTests } from "../recorded-test.js"
|
||||
|
||||
const meta = Meta.configure({ apiKey: process.env.META_API_KEY ?? "fixture" })
|
||||
const modelID = "muse-image-1.0"
|
||||
const recorded = recordedTests({
|
||||
prefix: "meta-images",
|
||||
provider: "meta",
|
||||
requires: ["META_API_KEY"],
|
||||
tags: ["image"],
|
||||
metadata: { model: modelID },
|
||||
})
|
||||
const controls = {
|
||||
reasoningStrength: "low",
|
||||
toolEnablement: { enable_image_search: false, enable_web_search: false, enable_shell: false },
|
||||
} as const
|
||||
|
||||
recorded.effect.with(
|
||||
"generates default WEBP bytes",
|
||||
{ protocol: "meta-images", tags: ["generation", "webp"] },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* Image.generate({
|
||||
model: meta.image(modelID),
|
||||
prompt: "A flat black square centered on a plain white background. No text.",
|
||||
options: { ...controls, n: 1, size: "256x256" },
|
||||
})
|
||||
expect(response.images).toHaveLength(1)
|
||||
expect(response.image?.mediaType).toBe("image/webp")
|
||||
expect(response.image?.data).toBeInstanceOf(Uint8Array)
|
||||
if (!(response.image?.data instanceof Uint8Array)) throw new Error("Expected image bytes")
|
||||
expect(new TextDecoder().decode(response.image.data.slice(0, 4))).toBe("RIFF")
|
||||
expect(new TextDecoder().decode(response.image.data.slice(8, 12))).toBe("WEBP")
|
||||
expect(response.usage?.outputTokens).toBeGreaterThan(0)
|
||||
}),
|
||||
180_000,
|
||||
)
|
||||
|
||||
recorded.effect.with(
|
||||
"edits image bytes and returns PNG",
|
||||
{ protocol: "meta-images", tags: ["editing", "png"] },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* Image.generate({
|
||||
model: meta.image(modelID),
|
||||
prompt: "Change the shape to bright purple. Keep the plain white background.",
|
||||
images: [
|
||||
ImageInput.bytes(
|
||||
yield* Effect.promise(() => Bun.file("test/fixtures/images/edit-source.jpg").bytes()),
|
||||
"image/jpeg",
|
||||
),
|
||||
],
|
||||
options: { ...controls, n: 1, outputFormat: "png", size: "256x256" },
|
||||
})
|
||||
expect(response.image?.mediaType).toBe("image/png")
|
||||
if (!(response.image?.data instanceof Uint8Array)) throw new Error("Expected image bytes")
|
||||
expect(Array.from(response.image.data.slice(0, 8))).toEqual([137, 80, 78, 71, 13, 10, 26, 10])
|
||||
}),
|
||||
180_000,
|
||||
)
|
||||
|
||||
recorded.effect.with(
|
||||
"replays a signed image handle for a Responses edit",
|
||||
{ protocol: "meta-responses", tags: ["hosted", "continuation", "editing"] },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const request = LLM.request({
|
||||
model: meta.responses(modelID),
|
||||
prompt: "A simple flat black square centered on a plain white background. No text.",
|
||||
tools: [
|
||||
Meta.imageGeneration({
|
||||
reasoningStrength: "low",
|
||||
enableImageSearch: false,
|
||||
enableWebSearch: false,
|
||||
enableShell: false,
|
||||
size: "1024x1024",
|
||||
}),
|
||||
],
|
||||
generation: { maxTokens: 4096 },
|
||||
})
|
||||
const compiled = yield* compileRequest(request)
|
||||
expect(compiled.body.tools).toMatchObject([
|
||||
{ type: "image_generation", reasoning_strength: "low", enable_web_search: false, size: "1024x1024" },
|
||||
])
|
||||
const first = yield* LLMClient.generate(request)
|
||||
const image = first.events.filter(LLMEvent.is.toolResult).find((event) => event.name === "image_generation")
|
||||
expect(image?.providerExecuted).toBe(true)
|
||||
expect(structuredClone(image?.result)).toMatchObject({
|
||||
type: "content",
|
||||
value: [{ type: "file", mime: "image/webp", uri: expect.stringMatching(/^data:image\/webp;base64,/) }],
|
||||
})
|
||||
const next = LLMRequest.update(request, {
|
||||
messages: [
|
||||
...request.messages,
|
||||
first.message,
|
||||
Message.user("Change the square to blue. Keep the white background."),
|
||||
],
|
||||
})
|
||||
const replay = yield* compileRequest(next)
|
||||
expect(replay.body.input).toEqual(
|
||||
expect.arrayContaining([{ type: "image_generation_call", id: image?.id, status: "completed", result: null }]),
|
||||
)
|
||||
expect(JSON.stringify(replay.body.input)).not.toContain("data:image")
|
||||
const second = yield* LLMClient.generate(next)
|
||||
expect(
|
||||
second.events.some(
|
||||
(event) => LLMEvent.is.toolResult(event) && event.name === "image_generation" && event.providerExecuted,
|
||||
),
|
||||
).toBe(true)
|
||||
expect(second.finishReason.normalized).toBe("stop")
|
||||
}),
|
||||
240_000,
|
||||
)
|
||||
@@ -0,0 +1,120 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMEvent, LLMRequest, Message, ToolChoice, ToolDefinition } from "../../src/index.js"
|
||||
import { Meta } from "../../src/providers/meta.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
import { recordedTests } from "../recorded-test.js"
|
||||
|
||||
const model = Meta.configure({ apiKey: process.env.META_API_KEY ?? "fixture" }).messages("muse-spark-1.3")
|
||||
const recorded = recordedTests({
|
||||
prefix: "meta-messages",
|
||||
provider: "meta",
|
||||
protocol: "meta-messages",
|
||||
requires: ["META_API_KEY"],
|
||||
metadata: { model: model.id },
|
||||
})
|
||||
|
||||
for (const mode of ["adaptive", "enabled"] as const) {
|
||||
recorded.effect.with(
|
||||
`streams text with ${mode} thinking`,
|
||||
{ tags: ["text", "reasoning", mode] },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const request = LLM.request({
|
||||
model,
|
||||
prompt: "What is 173 multiplied by 219? Reply with only the final integer.",
|
||||
generation: { maxTokens: 2048 },
|
||||
providerOptions: {
|
||||
thinking:
|
||||
mode === "adaptive"
|
||||
? { type: "adaptive", display: "omitted" }
|
||||
: { type: "enabled", budgetTokens: 1024, display: "omitted" },
|
||||
effort: "low",
|
||||
},
|
||||
})
|
||||
const compiled = yield* compileRequest(request)
|
||||
expect(compiled.body).toMatchObject({
|
||||
stream: true,
|
||||
max_tokens: 2048,
|
||||
thinking: { type: mode, display: "omitted" },
|
||||
output_config: { effort: "low" },
|
||||
})
|
||||
if (mode === "enabled") expect(compiled.body.thinking.budget_tokens).toBe(1024)
|
||||
const response = yield* LLMClient.generate(request)
|
||||
expect(response.text.replaceAll(",", "").trim()).toBe("37887")
|
||||
expect(response.finishReason.normalized).toBe("stop")
|
||||
expect(response.events.some(LLMEvent.is.textDelta)).toBe(true)
|
||||
expect(response.events.filter(LLMEvent.is.finish)).toHaveLength(1)
|
||||
expect(
|
||||
response.message.content.find((part) => part.type === "reasoning")?.providerMetadata?.meta?.redactedData,
|
||||
).toEqual(expect.stringMatching(/\S/))
|
||||
expect(response.usage?.reasoningTokens).toBeGreaterThan(0)
|
||||
}),
|
||||
90_000,
|
||||
)
|
||||
}
|
||||
|
||||
recorded.effect.with(
|
||||
"replays encrypted thinking through a tool loop",
|
||||
{ tags: ["tool", "tool-loop", "reasoning"] },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const request = LLM.request({
|
||||
model,
|
||||
prompt:
|
||||
"Look up the current weather in Paris using lookup_weather. After receiving the result, report Paris's weather in one short sentence.",
|
||||
tools: [
|
||||
ToolDefinition.make({
|
||||
name: "lookup_weather",
|
||||
description: "Look up current weather",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { city: { type: "string", enum: ["Paris"] } },
|
||||
required: ["city"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
toolChoice: "auto",
|
||||
generation: { maxTokens: 1024 },
|
||||
providerOptions: { effort: "low" },
|
||||
})
|
||||
const compiled = yield* compileRequest(request)
|
||||
expect(compiled.body.tool_choice).toEqual({ type: "auto" })
|
||||
const first = yield* LLMClient.generate(request)
|
||||
expect(first.finishReason.normalized).toBe("tool-calls")
|
||||
expect(first.toolCalls).toHaveLength(1)
|
||||
expect(first.toolCalls[0]).toMatchObject({ name: "lookup_weather", input: { city: "Paris" } })
|
||||
expect(first.events.some(LLMEvent.is.toolInputDelta)).toBe(true)
|
||||
const encrypted = first.message.content.find((part) => part.type === "reasoning")?.providerMetadata?.meta
|
||||
?.redactedData
|
||||
expect(encrypted).toEqual(expect.stringMatching(/\S/))
|
||||
const next = LLMRequest.update(request, {
|
||||
toolChoice: ToolChoice.make("none"),
|
||||
messages: [
|
||||
...request.messages,
|
||||
first.message,
|
||||
...first.toolCalls.map((call) =>
|
||||
Message.tool({ id: call.id, name: call.name, result: { condition: "sunny" } }),
|
||||
),
|
||||
],
|
||||
})
|
||||
const replay = yield* compileRequest(next)
|
||||
expect(replay.body.messages[1].content).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ type: "redacted_thinking", data: encrypted },
|
||||
expect.objectContaining({ type: "tool_use", id: first.toolCalls[0]?.id }),
|
||||
]),
|
||||
)
|
||||
expect(replay.body.messages[2].content).toMatchObject([
|
||||
{ type: "tool_result", tool_use_id: first.toolCalls[0]?.id, content: '{"condition":"sunny"}' },
|
||||
])
|
||||
expect(replay.body.tool_choice).toEqual({ type: "none" })
|
||||
const second = yield* LLMClient.generate(next)
|
||||
expect(second.text.toLowerCase()).toContain("sunny")
|
||||
expect(second.toolCalls).toHaveLength(0)
|
||||
expect(second.finishReason.normalized).toBe("stop")
|
||||
}),
|
||||
90_000,
|
||||
)
|
||||
@@ -0,0 +1,96 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMEvent, LLMRequest, Message } from "../../src/index.js"
|
||||
import { Meta } from "../../src/providers/meta.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
import { recordedTests } from "../recorded-test.js"
|
||||
|
||||
const meta = Meta.configure({ apiKey: process.env.META_API_KEY ?? "fixture" })
|
||||
const recorded = recordedTests({
|
||||
prefix: "meta-search",
|
||||
provider: "meta",
|
||||
requires: ["META_API_KEY"],
|
||||
tags: ["tool", "hosted", "web-search", "citation", "continuation"],
|
||||
metadata: { model: "muse-spark-1.3" },
|
||||
})
|
||||
|
||||
for (const api of ["responses", "messages"] as const) {
|
||||
recorded.effect.with(
|
||||
`searches and replays grounded ${api}`,
|
||||
{ protocol: `meta-${api}` },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const request = LLM.request({
|
||||
model: meta[api]("muse-spark-1.3"),
|
||||
prompt:
|
||||
"Use web search to find NASA's page identifying the first person to walk on the Moon. Answer in one sentence with a source citation.",
|
||||
tools: [Meta.webSearch()],
|
||||
generation: { maxTokens: 2048 },
|
||||
providerOptions:
|
||||
api === "responses"
|
||||
? { reasoningEffort: "low", include: ["reasoning.encrypted_content", "web_search_call.results"] }
|
||||
: { effort: "low" },
|
||||
})
|
||||
const compiled = yield* compileRequest(request)
|
||||
expect(compiled.body.tools).toEqual([
|
||||
api === "responses" ? { type: "web_search" } : { type: "web_search", name: "web_search" },
|
||||
])
|
||||
const first = yield* LLMClient.generate(request)
|
||||
expect(first.text.toLowerCase()).toContain("armstrong")
|
||||
expect(
|
||||
first.events.some(
|
||||
(event) => LLMEvent.is.toolCall(event) && event.providerExecuted && event.name === "web_search",
|
||||
),
|
||||
).toBe(true)
|
||||
expect(first.finishReason.normalized).toBe("stop")
|
||||
if (api === "responses") {
|
||||
const results = first.events
|
||||
.filter(LLMEvent.is.toolResult)
|
||||
.filter((event) => event.providerExecuted && event.name === "web_search")
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(structuredClone(results[0]?.result)).toMatchObject({
|
||||
type: "json",
|
||||
value: {
|
||||
type: "web_search_call",
|
||||
results: expect.arrayContaining([
|
||||
expect.objectContaining({ url: expect.any(String), title: expect.any(String) }),
|
||||
]),
|
||||
},
|
||||
})
|
||||
const annotations = first.message.content
|
||||
.filter((part) => part.type === "text")
|
||||
.flatMap((part) => part.providerMetadata?.meta?.annotations ?? [])
|
||||
expect(annotations).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ type: "url_citation", url: expect.stringMatching(/^https?:\/\//) }),
|
||||
]),
|
||||
)
|
||||
}
|
||||
const next = LLMRequest.update(request, {
|
||||
tools: [],
|
||||
messages: [
|
||||
...request.messages,
|
||||
first.message,
|
||||
Message.user("Using the information already found, reply with just that person's surname."),
|
||||
],
|
||||
})
|
||||
const replay = yield* compileRequest(next)
|
||||
if (api === "responses")
|
||||
expect(replay.body.input).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ type: "web_search_call", results: expect.any(Array) }),
|
||||
expect.objectContaining({ type: "reasoning", encrypted_content: expect.any(String) }),
|
||||
]),
|
||||
)
|
||||
if (api === "messages")
|
||||
expect(replay.body.messages[1].content).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ type: "server_tool_use", name: "web_search" })]),
|
||||
)
|
||||
const second = yield* LLMClient.generate(next)
|
||||
expect(second.text.toLowerCase()).toContain("armstrong")
|
||||
expect(second.finishReason.normalized).toBe("stop")
|
||||
}),
|
||||
120_000,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMEvent, LLMRequest, LLMResponse, Message, ToolDefinition } from "../../src/index.js"
|
||||
import { Meta } from "../../src/providers/meta.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
import { recordedTests } from "../recorded-test.js"
|
||||
|
||||
const meta = Meta.configure({ apiKey: process.env.META_API_KEY ?? "fixture" })
|
||||
const modelID = "muse-spark-1.3"
|
||||
const weather = ToolDefinition.make({
|
||||
name: "lookup_weather",
|
||||
description: "Look up the current weather for a city",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { city: { type: "string", enum: ["Paris"] } },
|
||||
required: ["city"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
})
|
||||
|
||||
for (const api of ["responses", "chat"] as const) {
|
||||
const recorded = recordedTests({
|
||||
prefix: `meta-${api}`,
|
||||
provider: "meta",
|
||||
protocol: api === "responses" ? "open-responses" : "openai-chat",
|
||||
requires: ["META_API_KEY"],
|
||||
metadata: { model: modelID },
|
||||
})
|
||||
|
||||
describe(`Meta ${api} recorded`, () => {
|
||||
for (const effort of [undefined, "minimal", "low", "medium", "high", "xhigh", "max"]) {
|
||||
recorded.effect.with(
|
||||
`streams text with ${effort ?? "default"} reasoning`,
|
||||
{ tags: ["text", "reasoning", "usage", `effort:${effort ?? "default"}`] },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const request = LLM.request({
|
||||
model: meta[api](modelID),
|
||||
prompt: "What is 173 multiplied by 219? Reply with only the final integer.",
|
||||
generation: { maxTokens: 1024 },
|
||||
providerOptions: {
|
||||
reasoningEffort: effort,
|
||||
...(api === "responses" && effort === "high" ? { reasoningSummary: "auto" } : {}),
|
||||
},
|
||||
})
|
||||
const compiled = yield* compileRequest(request)
|
||||
expect(compiled.body).toMatchObject({ model: modelID, stream: true })
|
||||
if (api === "responses") {
|
||||
expect(compiled.body).toMatchObject({
|
||||
max_output_tokens: 1024,
|
||||
store: false,
|
||||
include: ["reasoning.encrypted_content"],
|
||||
})
|
||||
expect(compiled.body.reasoning?.effort).toBe(effort)
|
||||
if (effort === "high") expect(compiled.body.reasoning.summary).toBe("auto")
|
||||
}
|
||||
if (api === "chat") {
|
||||
expect(compiled.body).toMatchObject({
|
||||
max_completion_tokens: 1024,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
expect(compiled.body.reasoning_effort).toBe(effort)
|
||||
expect(compiled.body.max_tokens).toBeUndefined()
|
||||
expect(compiled.body.store).toBeUndefined()
|
||||
}
|
||||
|
||||
const response = yield* LLMClient.generate(request)
|
||||
expect(response.text.replaceAll(",", "").trim()).toBe("37887")
|
||||
expect(response.finishReason.normalized).toBe("stop")
|
||||
expect(response.events.some(LLMEvent.is.textDelta)).toBe(true)
|
||||
expectUsage(response)
|
||||
if (api === "chat") expect(response.reasoning).toBe("")
|
||||
if (api === "responses") {
|
||||
const reasoning = response.message.content.find((part) => part.type === "reasoning")
|
||||
expect(reasoning?.providerMetadata?.meta?.reasoningEncryptedContent).toEqual(expect.stringMatching(/\S/))
|
||||
}
|
||||
}),
|
||||
90_000,
|
||||
)
|
||||
}
|
||||
|
||||
recorded.effect.with(
|
||||
api === "responses" ? "replays encrypted reasoning through a tool loop" : "continues a generated tool call",
|
||||
{ tags: ["tool", "tool-loop", "reasoning", "usage", "effort:low"] },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const request = LLM.request({
|
||||
model: meta[api](modelID),
|
||||
prompt:
|
||||
"Look up the current weather in Paris using lookup_weather before answering. After receiving the result, report Paris's weather in one short sentence.",
|
||||
tools: [weather],
|
||||
toolChoice: "auto",
|
||||
providerOptions: { reasoningEffort: "low" },
|
||||
generation: { maxTokens: 1024 },
|
||||
})
|
||||
const compiled = yield* compileRequest(request)
|
||||
expect(compiled.body.tool_choice).toBe("auto")
|
||||
expect(compiled.body.tools).toHaveLength(1)
|
||||
const first = yield* LLMClient.generate(request)
|
||||
expect(first.finishReason.normalized).toBe("tool-calls")
|
||||
expect(first.toolCalls).toHaveLength(1)
|
||||
expect(first.toolCalls[0]).toMatchObject({ name: "lookup_weather", input: { city: "Paris" } })
|
||||
expect(first.events.some(LLMEvent.is.toolInputDelta)).toBe(true)
|
||||
expectUsage(first)
|
||||
|
||||
const followUp = LLMRequest.update(request, {
|
||||
messages: [
|
||||
...request.messages,
|
||||
first.message,
|
||||
...first.toolCalls.map((call) =>
|
||||
Message.tool({ id: call.id, name: call.name, result: { condition: "sunny", temperature: "18C" } }),
|
||||
),
|
||||
],
|
||||
})
|
||||
const replay = yield* compileRequest(followUp)
|
||||
if (api === "responses") {
|
||||
const reasoning = first.message.content.find((part) => part.type === "reasoning")
|
||||
expect(reasoning?.providerMetadata?.meta?.reasoningEncryptedContent).toEqual(expect.stringMatching(/\S/))
|
||||
expect(replay.body.input).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: "reasoning",
|
||||
summary: [],
|
||||
encrypted_content: reasoning?.providerMetadata?.meta?.reasoningEncryptedContent,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: "function_call",
|
||||
call_id: first.toolCalls[0]?.id,
|
||||
name: "lookup_weather",
|
||||
arguments: '{"city":"Paris"}',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: "function_call_output",
|
||||
call_id: first.toolCalls[0]?.id,
|
||||
output: '{"condition":"sunny","temperature":"18C"}',
|
||||
}),
|
||||
]),
|
||||
)
|
||||
}
|
||||
if (api === "chat") {
|
||||
expect(first.reasoning).toBe("")
|
||||
expect(replay.body.messages).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
role: "assistant",
|
||||
tool_calls: [
|
||||
expect.objectContaining({
|
||||
id: first.toolCalls[0]?.id,
|
||||
function: { name: "lookup_weather", arguments: '{"city":"Paris"}' },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
{
|
||||
role: "tool",
|
||||
tool_call_id: first.toolCalls[0]?.id,
|
||||
content: '{"condition":"sunny","temperature":"18C"}',
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
const second = yield* LLMClient.generate(followUp)
|
||||
expect(second.finishReason.normalized).toBe("stop")
|
||||
expect(second.toolCalls).toHaveLength(0)
|
||||
expect(second.text).toContain("Paris")
|
||||
expect(second.text.toLowerCase()).toContain("sunny")
|
||||
expectUsage(second)
|
||||
}),
|
||||
90_000,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function expectUsage(response: LLMResponse) {
|
||||
expect(response.usage?.inputTokens).toBeGreaterThan(0)
|
||||
expect(response.usage?.outputTokens).toBeGreaterThan(0)
|
||||
expect(response.usage?.reasoningTokens).toBeGreaterThan(0)
|
||||
expect(response.events.filter(LLMEvent.is.finish)).toHaveLength(1)
|
||||
expect(response.events.filter(LLMEvent.is.stepFinish)).toHaveLength(1)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { expect } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Auth, LLM, LLMClient } from "../../src/index.js"
|
||||
import { Meta } from "../../src/providers/index.js"
|
||||
import { OpenAIChat } from "../../src/protocols/openai-chat.js"
|
||||
import { OpenResponses } from "../../src/protocols/open-responses.js"
|
||||
import { MetaResponses } from "../../src/protocols/meta-responses.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
import { dynamicResponse } from "../lib/http.js"
|
||||
import { sseEvents } from "../lib/sse.js"
|
||||
|
||||
it.effect("Meta composes baseline protocols with provider-owned endpoints and defaults", () =>
|
||||
Effect.gen(function* () {
|
||||
const meta = Meta.configure({ apiKey: "fixture" })
|
||||
const responses = meta.model("muse-spark-1.3")
|
||||
const chat = meta.chat("muse-spark-1.3")
|
||||
expect(responses.route.body).toBe(MetaResponses.protocol.body)
|
||||
expect(MetaResponses.protocol.stream.event).toBe(OpenResponses.protocol.stream.event)
|
||||
expect(chat.route.body).toBe(OpenAIChat.protocol.body)
|
||||
expect(meta.model).toBe(meta.responses)
|
||||
for (const model of [responses, chat]) {
|
||||
expect(model.provider).toBe("meta")
|
||||
expect(model.route.providerMetadataKey).toBe("meta")
|
||||
expect(model.route.endpoint.baseURL).toBe("https://api.meta.ai/v1")
|
||||
}
|
||||
expect(responses.route.endpoint.path).toBe("/responses")
|
||||
expect(chat.route.endpoint.path).toBe("/chat/completions")
|
||||
const compiled = yield* compileRequest(LLM.request({ model: responses, prompt: "Hello" }))
|
||||
expect(compiled.protocol).toBe("meta-responses")
|
||||
expect(compiled.body).toMatchObject({ store: false, include: ["reasoning.encrypted_content"] })
|
||||
expect(compiled.body.reasoning).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("Meta Responses stays on HTTP when a WebSocket executor is supplied", () =>
|
||||
Effect.gen(function* () {
|
||||
for (const baseURL of ["https://api.meta.ai/v1", "https://gateway.example/v1"]) {
|
||||
const response = yield* LLMClient.generate(
|
||||
LLM.request({
|
||||
model: Meta.configure({ apiKey: "fixture", baseURL }).responses("muse-spark-1.3"),
|
||||
prompt: "Hello",
|
||||
}),
|
||||
{ webSocket: { execute: () => Effect.die("Meta must not execute WebSocket requests") } },
|
||||
).pipe(
|
||||
Effect.provide(
|
||||
dynamicResponse((input) =>
|
||||
Effect.sync(() => {
|
||||
expect(input.request.method).toBe("POST")
|
||||
expect(input.request.url).toBe(`${baseURL}/responses`)
|
||||
expect(input.request.headers.authorization).toBe("Bearer fixture")
|
||||
expect(input.request.headers["openai-beta"]).toBeUndefined()
|
||||
expect(JSON.parse(input.text)).toMatchObject({ model: "muse-spark-1.3", stream: true })
|
||||
return input.respond(
|
||||
sseEvents(
|
||||
{ type: "response.created", response: { id: "resp_http" } },
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
output_index: 0,
|
||||
item: {
|
||||
id: "msg_http",
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "Hello" }],
|
||||
},
|
||||
},
|
||||
{ type: "response.completed", response: { id: "resp_http" } },
|
||||
),
|
||||
{ headers: { "content-type": "text/event-stream" } },
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
expect(response.text).toBe("Hello")
|
||||
expect(response.finishReason.normalized).toBe("stop")
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("Meta package selectors preserve overrides and Chat token policy on custom endpoints", () =>
|
||||
Effect.gen(function* () {
|
||||
for (const select of [Meta.model, Meta.chatModel]) {
|
||||
const model = select("future-model", {
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://gateway.example/v1",
|
||||
headers: { "x-client": "test" },
|
||||
body: { custom: "value" },
|
||||
providerOptions: { reasoningEffort: "future-effort" },
|
||||
})
|
||||
expect(model.route.endpoint.baseURL).toBe("https://gateway.example/v1")
|
||||
expect(model.route.defaults.headers).toEqual({ "x-client": "test" })
|
||||
expect(model.route.defaults.http?.body).toEqual({ custom: "value" })
|
||||
const compiled = yield* compileRequest(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
generation: { maxTokens: 64 },
|
||||
providerOptions: { store: true, include: [] },
|
||||
}),
|
||||
)
|
||||
if (select === Meta.model) {
|
||||
expect(compiled.body).toMatchObject({
|
||||
store: true,
|
||||
max_output_tokens: 64,
|
||||
reasoning: { effort: "future-effort" },
|
||||
})
|
||||
expect(compiled.body.include).toBeUndefined()
|
||||
}
|
||||
if (select === Meta.chatModel) {
|
||||
expect(compiled.body).toMatchObject({ max_completion_tokens: 64, reasoning_effort: "future-effort" })
|
||||
expect(compiled.body.max_tokens).toBeUndefined()
|
||||
expect(compiled.body.store).toBeUndefined()
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("Meta resolves environment credentials and accepts explicit auth overrides", () =>
|
||||
Effect.gen(function* () {
|
||||
for (const api of ["responses", "chat"] as const) {
|
||||
for (const scenario of [
|
||||
{ provider: Meta.configure(), authorization: "Bearer environment-key" },
|
||||
{ provider: Meta.configure({ apiKey: "explicit-key" }), authorization: "Bearer explicit-key" },
|
||||
{ provider: Meta.configure({ auth: Auth.none }), authorization: undefined },
|
||||
]) {
|
||||
const model = scenario.provider[api]("muse-spark-1.3")
|
||||
const headers = yield* model.route.auth.apply({
|
||||
request: LLM.request({ model, prompt: "Hello" }),
|
||||
method: "POST",
|
||||
url: "https://api.meta.ai/v1/responses",
|
||||
body: "{}",
|
||||
headers: Headers.empty,
|
||||
})
|
||||
expect(headers.authorization).toBe(scenario.authorization)
|
||||
}
|
||||
}
|
||||
}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: { META_API_KEY: "environment-key" } })))),
|
||||
)
|
||||
@@ -0,0 +1,265 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import {
|
||||
LLM,
|
||||
LLMEvent,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
Message,
|
||||
ToolChoice,
|
||||
ToolDefinition,
|
||||
type LanguageModel,
|
||||
} from "../../src/index.js"
|
||||
import { MiniMax } from "../../src/providers.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
import { recordedTests } from "../recorded-test.js"
|
||||
|
||||
const apiKey = process.env.MINIMAX_API_KEY ?? "fixture"
|
||||
const minimax = MiniMax.configure({ apiKey })
|
||||
const weather = ToolDefinition.make({
|
||||
name: "get_weather",
|
||||
description: "Get the current weather in a city",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { city: { type: "string", enum: ["Paris"] } },
|
||||
required: ["city"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
})
|
||||
|
||||
const textCases: ReadonlyArray<{
|
||||
name: string
|
||||
api: string
|
||||
protocol: string
|
||||
model: LanguageModel
|
||||
reasoning: boolean
|
||||
body: Record<string, unknown>
|
||||
}> = [
|
||||
{
|
||||
name: "M3 streams text with thinking disabled",
|
||||
api: "messages",
|
||||
protocol: "anthropic-messages",
|
||||
model: MiniMax.configure({ apiKey, providerOptions: { thinking: { type: "disabled" } } }).model("MiniMax-M3"),
|
||||
reasoning: false,
|
||||
body: { thinking: { type: "disabled" } },
|
||||
},
|
||||
{
|
||||
name: "M3 streams adaptive thinking",
|
||||
api: "messages",
|
||||
protocol: "anthropic-messages",
|
||||
model: MiniMax.configure({ apiKey, providerOptions: { thinking: { type: "adaptive" } } }).messages("MiniMax-M3"),
|
||||
reasoning: true,
|
||||
body: { thinking: { type: "adaptive" } },
|
||||
},
|
||||
{
|
||||
name: "M2.7 streams default thinking",
|
||||
api: "messages",
|
||||
protocol: "anthropic-messages",
|
||||
model: minimax.model("MiniMax-M2.7"),
|
||||
reasoning: true,
|
||||
body: {},
|
||||
},
|
||||
{
|
||||
name: "M3 streams text with thinking disabled",
|
||||
api: "chat",
|
||||
protocol: "minimax-chat",
|
||||
model: MiniMax.configure({ apiKey, providerOptions: { thinking: { type: "disabled" } } }).chat("MiniMax-M3"),
|
||||
reasoning: false,
|
||||
body: { thinking: { type: "disabled" }, reasoning_split: true },
|
||||
},
|
||||
{
|
||||
name: "M3 streams text with effort none",
|
||||
api: "responses",
|
||||
protocol: "open-responses",
|
||||
model: MiniMax.configure({ apiKey, providerOptions: { reasoningEffort: "none" } }).responses("MiniMax-M3"),
|
||||
reasoning: false,
|
||||
body: { reasoning: { effort: "none" } },
|
||||
},
|
||||
]
|
||||
|
||||
describe("MiniMax recorded", () => {
|
||||
for (const item of textCases) {
|
||||
const recorded = recordedTests({
|
||||
prefix: `minimax-${item.api}`,
|
||||
provider: "minimax",
|
||||
protocol: item.protocol,
|
||||
requires: ["MINIMAX_API_KEY"],
|
||||
metadata: { model: item.model.id },
|
||||
})
|
||||
recorded.effect.with(
|
||||
item.name,
|
||||
{ tags: ["text", "usage", item.reasoning ? "reasoning" : "thinking-off"] },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const request = LLM.request({
|
||||
model: item.model,
|
||||
prompt: "What is 173 multiplied by 219? Reply with only the final integer.",
|
||||
generation: { maxTokens: 1536 },
|
||||
})
|
||||
const compiled = yield* compileRequest(request)
|
||||
expect(compiled.body).toMatchObject(item.body)
|
||||
|
||||
const response = yield* LLMClient.generate(request)
|
||||
expect(response.text.replaceAll(",", "").trim()).toBe("37887")
|
||||
expect(response.text).not.toContain("<think>")
|
||||
expect(response.reasoning.length > 0).toBe(item.reasoning)
|
||||
expect(response.events.some(LLMEvent.is.reasoningDelta)).toBe(item.reasoning)
|
||||
expect(response.events.some(LLMEvent.is.textDelta)).toBe(true)
|
||||
expectUsage(response)
|
||||
}),
|
||||
60_000,
|
||||
)
|
||||
}
|
||||
|
||||
const messages = recordedTests({
|
||||
prefix: "minimax-messages",
|
||||
provider: "minimax",
|
||||
protocol: "anthropic-messages",
|
||||
requires: ["MINIMAX_API_KEY"],
|
||||
metadata: { model: "MiniMax-M3" },
|
||||
})
|
||||
|
||||
messages.effect.with(
|
||||
"M3 generates a named tool call with default thinking off",
|
||||
{ tags: ["tool", "thinking-off", "usage"] },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(
|
||||
LLM.request({
|
||||
model: minimax.model("MiniMax-M3"),
|
||||
prompt: "Use get_weather to look up the current weather in Paris.",
|
||||
tools: [weather],
|
||||
toolChoice: ToolChoice.named("get_weather"),
|
||||
generation: { maxTokens: 512 },
|
||||
}),
|
||||
)
|
||||
expect(response.finishReason.normalized).toBe("tool-calls")
|
||||
expect(response.toolCalls).toMatchObject([{ name: "get_weather", input: { city: "Paris" } }])
|
||||
expect(response.reasoning).toBe("")
|
||||
expect(response.events.some(LLMEvent.is.toolInputDelta)).toBe(true)
|
||||
expectUsage(response)
|
||||
}),
|
||||
60_000,
|
||||
)
|
||||
|
||||
const loops: ReadonlyArray<{ api: string; protocol: string; mode: string; model: LanguageModel }> = [
|
||||
{
|
||||
api: "messages",
|
||||
protocol: "anthropic-messages",
|
||||
mode: "adaptive thinking",
|
||||
model: MiniMax.configure({ apiKey, providerOptions: { thinking: { type: "adaptive" } } }).model("MiniMax-M3"),
|
||||
},
|
||||
{
|
||||
api: "chat",
|
||||
protocol: "minimax-chat",
|
||||
mode: "default thinking",
|
||||
model: minimax.chat("MiniMax-M3"),
|
||||
},
|
||||
{
|
||||
api: "responses",
|
||||
protocol: "open-responses",
|
||||
mode: "effort minimal",
|
||||
model: MiniMax.configure({ apiKey, providerOptions: { reasoningEffort: "minimal" } }).responses("MiniMax-M3"),
|
||||
},
|
||||
]
|
||||
|
||||
for (const item of loops) {
|
||||
const recorded = recordedTests({
|
||||
prefix: `minimax-${item.api}`,
|
||||
provider: "minimax",
|
||||
protocol: item.protocol,
|
||||
requires: ["MINIMAX_API_KEY"],
|
||||
metadata: { model: item.model.id },
|
||||
})
|
||||
recorded.effect.with(
|
||||
`M3 continues a tool loop with ${item.mode}`,
|
||||
{ tags: ["tool", "tool-loop", "reasoning", "continuation", "usage"] },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const request = LLM.request({
|
||||
model: item.model,
|
||||
prompt:
|
||||
"Look up the current weather in Paris using get_weather before answering. After receiving the result, report the weather in one short sentence.",
|
||||
tools: [weather],
|
||||
toolChoice: "auto",
|
||||
generation: { maxTokens: 1536 },
|
||||
})
|
||||
const first = yield* LLMClient.generate(request)
|
||||
expect(first.finishReason.normalized).toBe("tool-calls")
|
||||
expect(first.toolCalls).toHaveLength(1)
|
||||
expect(first.toolCalls).toMatchObject([{ name: "get_weather", input: { city: "Paris" } }])
|
||||
expect(first.reasoning.length).toBeGreaterThan(0)
|
||||
expect(first.events.some(LLMEvent.is.reasoningDelta)).toBe(true)
|
||||
expectUsage(first)
|
||||
|
||||
const followUp = LLMRequest.update(request, {
|
||||
toolChoice: ToolChoice.make("none"),
|
||||
messages: [
|
||||
...request.messages,
|
||||
first.message,
|
||||
...first.toolCalls.map((call) =>
|
||||
Message.tool({ id: call.id, name: call.name, result: { condition: "sunny", temperature: "18C" } }),
|
||||
),
|
||||
],
|
||||
})
|
||||
const replay = yield* compileRequest(followUp)
|
||||
const reasoning = first.message.content.filter((part) => part.type === "reasoning")
|
||||
if (item.api === "messages") {
|
||||
reasoning.forEach((part) => expect(part.providerMetadata?.minimax?.signature).toEqual(expect.any(String)))
|
||||
expect(replay.body.messages).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
role: "assistant",
|
||||
content: expect.arrayContaining(
|
||||
reasoning.map((part) => ({
|
||||
type: "thinking",
|
||||
thinking: part.text,
|
||||
signature: part.providerMetadata?.minimax?.signature,
|
||||
})),
|
||||
),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
}
|
||||
if (item.api === "chat") {
|
||||
expect(replay.body.messages).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
role: "assistant",
|
||||
reasoning_content: first.reasoning,
|
||||
reasoning_details: reasoning.flatMap(
|
||||
(part) => part.providerMetadata?.minimax?.reasoningDetails ?? [],
|
||||
),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
}
|
||||
if (item.api === "responses") {
|
||||
expect(replay.body.input).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: "reasoning",
|
||||
summary: expect.arrayContaining([{ type: "summary_text", text: first.reasoning }]),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
const second = yield* LLMClient.generate(followUp)
|
||||
expect(second.finishReason.normalized).toBe("stop")
|
||||
expect(second.toolCalls).toHaveLength(0)
|
||||
expect(second.text).toContain("Paris")
|
||||
expect(second.text.toLowerCase()).toContain("sunny")
|
||||
expectUsage(second)
|
||||
}),
|
||||
120_000,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function expectUsage(response: LLMResponse) {
|
||||
expect(response.usage?.inputTokens).toBeGreaterThan(0)
|
||||
expect(response.usage?.outputTokens).toBeGreaterThan(0)
|
||||
expect(response.events.filter(LLMEvent.is.finish)).toHaveLength(1)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { LLM } from "../../src/index.js"
|
||||
import { MiniMax } from "../../src/providers.js"
|
||||
import { AnthropicMessages } from "../../src/protocols/anthropic-messages.js"
|
||||
import { OpenResponses } from "../../src/protocols/open-responses.js"
|
||||
import { Auth } from "../../src/route/auth.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
import { Endpoint } from "../../src/route/endpoint.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
|
||||
describe("MiniMax provider", () => {
|
||||
test("composes the baseline Messages and Responses protocols", () => {
|
||||
const minimax = MiniMax.configure()
|
||||
expect(minimax.model).toBe(minimax.messages)
|
||||
expect(minimax.model("MiniMax-M3").route.body).toBe(AnthropicMessages.protocol.body)
|
||||
expect(minimax.responses("MiniMax-M3").route.body).toBe(OpenResponses.protocol.body)
|
||||
})
|
||||
|
||||
it.effect("owns API endpoints, provider identity and environment bearer authentication", () =>
|
||||
Effect.gen(function* () {
|
||||
const minimax = MiniMax.configure()
|
||||
for (const item of [
|
||||
{ model: minimax.model("MiniMax-M3"), path: "/anthropic/v1/messages" },
|
||||
{ model: minimax.chat("MiniMax-M3"), path: "/v1/chat/completions" },
|
||||
{ model: minimax.responses("MiniMax-M3"), path: "/v1/responses" },
|
||||
]) {
|
||||
const request = LLM.request({ model: item.model, prompt: "Hello" })
|
||||
const compiled = yield* compileRequest(request)
|
||||
expect(item.model.provider).toBe("minimax")
|
||||
expect(item.model.route.providerMetadataKey).toBe("minimax")
|
||||
const url = Endpoint.render(item.model.route.endpoint, { request, body: compiled.body }).toString()
|
||||
expect(url).toBe(`https://api.minimax.io${item.path}`)
|
||||
const headers = yield* item.model.route.auth.apply({
|
||||
request,
|
||||
method: "POST",
|
||||
url,
|
||||
body: "{}",
|
||||
headers: Headers.empty,
|
||||
})
|
||||
expect(headers.authorization).toBe("Bearer fixture-key")
|
||||
expect(headers["x-api-key"]).toBeUndefined()
|
||||
}
|
||||
}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: { MINIMAX_API_KEY: "fixture-key" } })))),
|
||||
)
|
||||
|
||||
it.effect("honors explicit auth and custom API bases", () =>
|
||||
Effect.gen(function* () {
|
||||
const model = MiniMax.configure({
|
||||
baseURL: "https://gateway.example/anthropic/v1",
|
||||
auth: Auth.header("x-api-key", "gateway-key"),
|
||||
}).model("custom-model")
|
||||
const request = LLM.request({ model, prompt: "Hello" })
|
||||
const compiled = yield* compileRequest(request)
|
||||
expect(Endpoint.render(model.route.endpoint, { request, body: compiled.body }).toString()).toBe(
|
||||
"https://gateway.example/anthropic/v1/messages",
|
||||
)
|
||||
expect(model.route.headers?.({ request })).toEqual({ "anthropic-version": "2023-06-01" })
|
||||
const headers = yield* model.route.auth.apply({
|
||||
request,
|
||||
method: "POST",
|
||||
url: "https://gateway.example/anthropic/v1/messages",
|
||||
body: "{}",
|
||||
headers: Headers.empty,
|
||||
})
|
||||
expect(headers["x-api-key"]).toBe("gateway-key")
|
||||
expect(headers.authorization).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps thinking controls native to the selected API", () =>
|
||||
Effect.gen(function* () {
|
||||
const minimax = MiniMax.configure({ apiKey: "fixture" })
|
||||
const messages = yield* compileRequest(
|
||||
LLM.request({ model: minimax.model("MiniMax-M3"), providerOptions: { thinking: { type: "adaptive" } } }),
|
||||
)
|
||||
expect(messages.body.thinking).toEqual({ type: "adaptive" })
|
||||
expect(messages.body.output_config).toBeUndefined()
|
||||
const chat = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: minimax.chat("MiniMax-M3"),
|
||||
generation: { maxTokens: 128 },
|
||||
providerOptions: { thinking: { type: "disabled" }, reasoningSplit: false },
|
||||
}),
|
||||
)
|
||||
expect(chat.body).toMatchObject({
|
||||
thinking: { type: "disabled" },
|
||||
reasoning_split: false,
|
||||
max_completion_tokens: 128,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
expect(chat.body.store).toBeUndefined()
|
||||
const responses = yield* compileRequest(
|
||||
LLM.request({ model: minimax.responses("MiniMax-M3"), providerOptions: { reasoningEffort: "minimal" } }),
|
||||
)
|
||||
expect(responses.body.reasoning).toEqual({ effort: "minimal" })
|
||||
expect(responses.body.include).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -19,7 +19,7 @@ const chunk = (delta: object, finishReason: string | null = null, usage?: object
|
||||
|
||||
describe("Mistral Chat", () => {
|
||||
test("exposes native provider and protocol identities", async () => {
|
||||
const entrypoint = await import("@opencode-ai/ai/providers/mistral")
|
||||
const entrypoint = await import("@opencode/ai/providers/mistral")
|
||||
|
||||
expect(Mistral.id).toBe("mistral")
|
||||
expect(MistralChat.protocol.id).toBe("mistral-chat")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { configure } from "@opencode-ai/ai/providers/mistral"
|
||||
import { configure } from "@opencode/ai/providers/mistral"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMEvent, LLMRequest, Message, ToolChoice, ToolDefinition } from "../../src/index.js"
|
||||
|
||||
@@ -34,7 +34,7 @@ const observationFrame = (observation: ChannelObservation) => {
|
||||
|
||||
const terminal = (observation: ChannelObservation) => observation.type !== "frame"
|
||||
|
||||
// This deliberately models only sequential test traffic. Core owns production connection pooling and recovery.
|
||||
// This channel fixture supports sequential test traffic.
|
||||
const makeChannel = Effect.gen(function* () {
|
||||
const constructor = yield* Socket.WebSocketConstructor
|
||||
let connection: WebSocketConnection | undefined
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HttpRecorder } from "@opencode-ai/http-recorder"
|
||||
import type { HttpRecorder } from "@opencode/http-recorder"
|
||||
import { describe } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import type { LanguageModel } from "../src/index.js"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HttpRecorder } from "@opencode-ai/http-recorder"
|
||||
import type { HttpRecorder } from "@opencode/http-recorder"
|
||||
import { test, type TestOptions } from "bun:test"
|
||||
import { Effect, type Layer } from "effect"
|
||||
import { testEffect } from "./lib/effect.js"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HttpRecorder } from "@opencode-ai/http-recorder"
|
||||
import { HttpRecorder } from "@opencode/http-recorder"
|
||||
import { NodeSocket } from "@effect/platform-node"
|
||||
import { Layer } from "effect"
|
||||
import { Socket } from "effect/unstable/socket"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Content } from "@opencode-ai/schema/tool"
|
||||
import { Content } from "@opencode/schema/tool"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import {
|
||||
GenerationOptions,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TimelineRow } from "@opencode-ai/session-ui/timeline/projection"
|
||||
import { TimelineRow } from "@opencode/session-ui/timeline/projection"
|
||||
import { onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { render } from "solid-js/web"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Service } from "@opencode-ai/client/service"
|
||||
import { Service } from "@opencode/client/service"
|
||||
import { chromium, expect, type Browser, type Page, type TestInfo } from "@playwright/test"
|
||||
import { spawn, spawnSync, type ChildProcess } from "node:child_process"
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
|
||||
import type { SessionMessageInfo } from "@opencode/client/promise"
|
||||
import { benchmark, expect } from "../benchmark"
|
||||
import { mockOpenCodeServer } from "../../utils/mock-server"
|
||||
import { fixture } from "../timeline/session-timeline-stress.fixture"
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import { render } from "solid-js/web"
|
||||
import { Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { ThemeProvider } from "@opencode-ai/ui/theme"
|
||||
import { ThemeProvider } from "@opencode/ui/theme"
|
||||
import { CurrentSessionProviders } from "../../../../session-ui/src/storybook/current-session-story"
|
||||
import { emptySessionDocument } from "../../../../session-ui/src/storybook/current-session-fixtures"
|
||||
import { CurrentFileToolGroup, ToolDisplay } from "../../../../session-ui/src/tools/tool-renderer"
|
||||
import { patchFileGroups } from "../../../../session-ui/src/components/apply-patch-file"
|
||||
import type { SessionMessageAssistantTool } from "@opencode-ai/client/promise"
|
||||
import type { SessionMessageAssistantTool } from "@opencode/client/promise"
|
||||
import { createTwoFilesPatch, diffLines } from "diff"
|
||||
import edit from "../../../../core/src/tool/plugin/edit.ts?raw"
|
||||
import patch from "../../../../core/src/tool/plugin/patch.ts?raw"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user