Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions docs/web-search-provider-capability.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@

# Provider-hosted web search capability

Status: implemented for OpenAI Responses and Anthropic Messages
`web_search_20250305`; other provider-native wires remain explicit follow-ups.
Status: implemented for OpenAI Responses, Alibaba Token Plan Open Responses,
and Anthropic Messages `web_search_20250305`; other provider-native wires
remain explicit follow-ups.
Verified against public provider documentation and shipped client/SDK behavior
on 2026-08-04.
through 2026-08-24.

## Problem

Expand Down Expand Up @@ -59,8 +60,15 @@ set `capabilities.webSearch`; otherwise narrow provider/model rules apply.
The implemented native adapters are:

- `openai-responses` for Codex-style `web_search`;
- `openai-responses` over the Alibaba Token Plan Open Responses profile for
`qwen3.8-max` Harness `web_search`;
- `anthropic-messages` for Claude Code-compatible `web_search_20250305`.

Alibaba Token Plan stays deliberately narrower than the provider's published
model matrix: the current Runtime routes only `qwen3.8-max` through its verified
Responses profile. Qwen 3.7 models remain on Chat in Maka and therefore do not
receive a provider-native search tool yet.

## Execution surfaces

All production `AiSdkBackend` composition roots use the same
Expand Down Expand Up @@ -105,8 +113,11 @@ turn-start tool surface
The search request uses the existing model credential. It does not duplicate the
secret into web-search settings, start a nested model request, or expose the
secret to the renderer. Provider search call/results are marked
`providerExecuted`, preserved separately from local ToolRuntime execution, and
replayed through the native provider-tool shape.
`providerExecuted` and preserved separately from local ToolRuntime execution.
Adapters that can round-trip the pair replay its native provider-tool shape.
Alibaba Open Responses currently keeps the durable episode and grounded text but
omits the pair from the next provider request, because that adapter cannot yet
round-trip provider-executed results without producing a dangling output.

The durable event keeps normalized `result` data for the canonical read model,
UI, and exports, plus opaque `providerOutput` data for provider-protocol replay.
Expand All @@ -125,6 +136,15 @@ stream. DeepSeek returned search actions but no structured source rows or URL
annotations in that response, so Maka preserves citations when supplied but
does not synthesize or invent them.

After DeepSeek moved to the plaintext `@ai-sdk/open-responses` adapter, native
search was temporarily disabled because that adapter dropped provider-defined
tools. Live revalidation on 2026-08-24 against first-party
`deepseek-v4-flash` completed six provider-executed actions (`search`,
`open_page`, and `find_in_page`) and a grounded final answer. The shared Open
Responses mapper preserves those actions while the existing replay boundary
continues to omit the provider-owned pair from the next request instead of
emitting a dangling function output.

The same key was also verified through DeepSeek's Anthropic-compatible endpoint:
one real Maka turn sent `web_search_20250305`, received one provider-executed
WebSearch result containing ten source rows, and completed the final answer
Expand Down Expand Up @@ -187,7 +207,8 @@ search-heavy workflows that value source visibility over cache economics.
| OpenAI API | Responses `web_search` tool | Maka currently enables the native path for GPT-5 families, whose runtime wire is already Responses | Integrated through `openai-responses` |
| Custom Responses relay | Responses `web_search` tool when explicitly declared by model metadata | `openai-responses-compatible` connections with `apiProtocol=openai-responses` and `capabilities.webSearch=true` | Integrated through `openai-responses` |
| xAI API / OAuth | Responses Agent Tools `web_search` | Maka currently enables the verified Grok 4.5 Responses route | Integrated through `openai-responses` |
| Alibaba Model Studio | Responses `web_search` | Qwen 3.5 Plus/Flash provider support is recorded | Provider supports it; Maka Responses adapter pending |
| Alibaba Token Plan | Responses Harness `web_search` | `qwen3.8-max` on the Token Plan China/Singapore access paths | Integrated through the Alibaba Open Responses profile; live smoke pending |
| Alibaba Model Studio pay-as-you-go | Responses `web_search` | Provider model and deployment support varies | Provider supports it; Maka pay-as-you-go Responses adapter pending |
| Anthropic / Claude subscription | Messages `web_search_20250305` | Current Claude Opus/Sonnet/Haiku/Fable families | Integrated through `anthropic-messages` |
| MiniMax API / Coding Plan | Anthropic-compatible `web_search_20250305` | MiniMax M2.7/M3 families | Integrated through `anthropic-messages`; live provider verification pending |
| Google Gemini | Gemini API grounding with Google Search | Supported Gemini 2.0+ model families vary by release | Provider supports it; Maka adapter pending |
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/__tests__/model-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,12 @@ describe('openAiAdapterApiProtocol', () => {
assert.equal(openAiAdapterApiProtocol('muse-spark-1.2-contributor', 'opencode'), 'openai-chat');
assert.equal(openAiAdapterApiProtocol('minimax-m3', 'opencode-go'), 'openai-chat');
});

it('routes only Qwen3.8 Max through Alibaba Token Plan Responses', () => {
for (const providerType of ['alibaba-token-plan-cn', 'alibaba-token-plan'] as const) {
assert.equal(openAiAdapterApiProtocol('qwen3.8-max', providerType), 'openai-responses');
assert.equal(openAiAdapterApiProtocol('qwen3.7-max', providerType), 'openai-chat');
}
assert.equal(openAiAdapterApiProtocol('qwen3.8-max', 'alibaba-cn'), 'openai-chat');
});
});
27 changes: 24 additions & 3 deletions packages/core/src/__tests__/model-web-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ describe('hosted web search capability', () => {
it('enables the implemented Responses path only for declared model families', () => {
assert.deepEqual(resolveHostedWebSearchCapability('deepseek', undefined, 'deepseek-v4-flash'), {
adapter: 'openai-responses',
implemented: false,
implemented: true,
});
assert.deepEqual(resolveHostedWebSearchCapability('deepseek', undefined, 'deepseek-v4-pro'), {
adapter: 'openai-responses',
implemented: false,
implemented: true,
});
assert.equal(resolveHostedWebSearchCapability('deepseek', undefined, 'deepseek-chat'), null);
assert.deepEqual(resolveHostedWebSearchCapability('openai', undefined, 'gpt-5.5'), {
Expand All @@ -49,6 +49,19 @@ describe('hosted web search capability', () => {
adapter: 'openai-responses',
implemented: false,
});
for (const providerType of ['alibaba-token-plan-cn', 'alibaba-token-plan'] as const) {
assert.deepEqual(resolveHostedWebSearchCapability(providerType, undefined, 'qwen3.8-max'), {
adapter: 'openai-responses',
implemented: true,
});
assert.equal(resolveHostedWebSearchCapability(providerType, undefined, 'qwen3.7-max'), null);
assert.equal(resolveHostedWebSearchCapability(providerType, undefined, 'qwen3.7-plus'), null);
assert.equal(
resolveHostedWebSearchCapability(providerType, undefined, 'qwen3.8-max-preview'),
null,
);
assert.equal(resolveHostedWebSearchCapability(providerType, undefined, 'qwen3.6-plus'), null);
}
assert.equal(resolveHostedWebSearchCapability('openai', undefined, 'gpt-4.1'), null);
});

Expand Down Expand Up @@ -124,7 +137,7 @@ describe('hosted web search capability', () => {
it('keeps dual-wire providers on the configured connection protocol', () => {
assert.deepEqual(resolveHostedWebSearchCapability('deepseek', undefined, 'deepseek-v4-flash'), {
adapter: 'openai-responses',
implemented: false,
implemented: true,
});
assert.deepEqual(
resolveHostedWebSearchCapability(
Expand Down Expand Up @@ -155,6 +168,14 @@ describe('hosted web search capability', () => {
),
null,
);
assert.equal(
resolveHostedWebSearchCapability(
'alibaba-token-plan-cn',
[{ id: 'qwen3.8-max', apiProtocol: 'openai-chat' }],
'qwen3.8-max',
),
null,
);
});

it('publishes native search for both first-party DeepSeek V4 models', () => {
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/__tests__/provider-catalog-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ describe('provider catalog contract — structural invariants over CATALOG_PROVI
);
}
});

it('delegates Alibaba Token Plan execution through one explicit Runtime profile', () => {
const delegated = Object.entries(PROVIDER_REGISTRY).flatMap(([providerType, definition]) => {
const adapter = definition.runtimeAdapter;
return adapter.kind === 'openai-compatible' && adapter.runtimeProfile
? [{ providerType, runtimeProfile: adapter.runtimeProfile }]
: [];
});
assert.deepEqual(delegated, [
{ providerType: 'alibaba-token-plan-cn', runtimeProfile: 'alibaba-token-plan' },
{ providerType: 'alibaba-token-plan', runtimeProfile: 'alibaba-token-plan' },
]);
});
});

describe('retired provider contract', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/llm-connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
type ProviderCategory,
type ProviderDefaults,
type ProviderRuntimeAdapter,
type ProviderRuntimeProfileId,
type ProviderResponsesContract,
type ProviderType,
} from './provider-registry.js';
Expand All @@ -64,6 +65,7 @@ export type {
ProviderCategory,
ProviderDefaults,
ProviderRuntimeAdapter,
ProviderRuntimeProfileId,
ProviderResponsesContract,
ProviderType,
};
Expand Down
11 changes: 6 additions & 5 deletions packages/core/src/model-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,10 @@ export function lookupModelProviderOverride(
/**
* The request wire a model served over the OpenAI adapter must use.
*
* OpenAI's `gpt-5*` families and xAI's `grok-4.5` are served only over the
* Responses API; every other model on the native OpenAI adapter uses Chat
* Completions. This is the single declared source of that protocol split,
* expressed through the {@link ModelInfo.apiProtocol} seam. It is consumed by
* the runtime model factory and the conformance matrix.
* Provider/model routing facts live here even when the concrete Responses SDK
* and replay policy are delegated to a Runtime profile. This is the single
* declared source of the default protocol split, expressed through the
* {@link ModelInfo.apiProtocol} seam.
*/
export function openAiAdapterApiProtocol(
modelId: string,
Expand All @@ -138,6 +137,8 @@ export function openAiAdapterApiProtocol(
const id = modelId.trim();
return (providerType === 'deepseek' && deepSeekModelSupportsResponses(id)) ||
(providerType === 'opencode-go' && id === 'muse-spark-1.2-contributor') ||
((providerType === 'alibaba-token-plan-cn' || providerType === 'alibaba-token-plan') &&
id === 'qwen3.8-max') ||
/^gpt-5/i.test(id) ||
((providerType === 'xai' || providerType === 'xai-oauth') && id === 'grok-4.5')
? 'openai-responses'
Expand Down
13 changes: 9 additions & 4 deletions packages/core/src/model-web-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,7 @@ function providerHostedWebSearchAdapter(
): HostedWebSearchCapability | null {
switch (providerType) {
case 'deepseek':
// @ai-sdk/open-responses currently serializes function tools only.
// Mark native search unavailable so routing never hands it a provider
// tool that would be silently filtered from the request.
return { adapter: 'openai-responses', implemented: false };
return { adapter: 'openai-responses', implemented: true };
case 'openai':
case 'openai-responses-compatible':
case 'xai':
Expand All @@ -89,6 +86,9 @@ function providerHostedWebSearchAdapter(
case 'alibaba':
case 'alibaba-cn':
return { adapter: 'openai-responses', implemented: false };
case 'alibaba-token-plan-cn':
case 'alibaba-token-plan':
return { adapter: 'openai-responses', implemented: true };
case 'anthropic':
case 'MiniMax':
case 'MiniMax-cn':
Expand Down Expand Up @@ -127,6 +127,11 @@ function providerDefaultHostedWebSearchCapability(
case 'alibaba':
case 'alibaba-cn':
return /^qwen3\.5-(?:plus|flash)(?:[.-]|$)/i.test(modelId) ? capability : null;
case 'alibaba-token-plan-cn':
case 'alibaba-token-plan':
// Keep the hosted tool on the exact model that the Token Plan runtime
// routes through Responses. Aliases and Qwen 3.7 still use Chat here.
return modelId === 'qwen3.8-max' ? capability : null;
case 'anthropic':
return /^claude-(?:[\d.]+-)*(?:opus|sonnet|haiku|fable)\b/i.test(modelId) ? capability : null;
case 'MiniMax':
Expand Down
53 changes: 41 additions & 12 deletions packages/core/src/provider-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ export type ProviderCatalogGroup = 'recommended' | 'plans' | 'api' | 'aggregator

export type ApplyPatchProtocol = 'openai-structured' | 'codex-v4a-freeform';

/**
* Stable reference to provider execution policy implemented by `@maka/runtime`.
* Core owns only this protocol-level delegation; SDK selection, replay
* carriers, and request mutation remain Runtime implementation details.
*/
export type ProviderRuntimeProfileId = 'alibaba-token-plan';

export type ProviderResponsesContract =
| {
readonly adapter: 'openai';
Expand All @@ -41,6 +48,29 @@ export type ProviderResponsesContract =
readonly reasoningReplay: 'plaintext-content';
};

type OpenAiCompatibleRuntimeAdapterBase = {
kind: 'openai-compatible';
name: 'provider' | 'connection';
includeUsage?: boolean;
requireBaseUrl?: boolean;
replayAssistantReasoningAs?: 'reasoning';
replayAssistantReasoningDetails?: true;
};

type OpenAiCompatibleRuntimeAdapter = OpenAiCompatibleRuntimeAdapterBase &
(
| {
/** Presence enables a complete Core-owned Responses contract. */
responses?: ProviderResponsesContract;
runtimeProfile?: never;
}
| {
responses?: never;
/** Explicitly delegates concrete execution policy to `@maka/runtime`. */
runtimeProfile: ProviderRuntimeProfileId;
}
);

type ProviderRuntimeAdapterDefinition =
| { kind: 'anthropic'; auth: 'api-key' | 'bearer'; normalizeBaseUrl: boolean }
/**
Expand All @@ -53,16 +83,7 @@ type ProviderRuntimeAdapterDefinition =
| { kind: 'google'; normalizeBaseUrl?: boolean }
| { kind: 'github-copilot' }
| { kind: 'cohere' }
| {
kind: 'openai-compatible';
name: 'provider' | 'connection';
includeUsage?: boolean;
requireBaseUrl?: boolean;
/** Presence enables Responses and fixes the only supported SDK/replay pairing. */
responses?: ProviderResponsesContract;
replayAssistantReasoningAs?: 'reasoning';
replayAssistantReasoningDetails?: true;
};
| OpenAiCompatibleRuntimeAdapter;

export type ProviderRuntimeAdapter = ProviderRuntimeAdapterDefinition & {
/** Provider wire contract for ApplyPatch. Model support is resolved separately. */
Expand Down Expand Up @@ -1664,7 +1685,11 @@ const providerRegistry = {
fallbackModels: [...alibabaTokenPlanModelIds],
status: 'ready',
protocol: 'openai',
runtimeAdapter: { kind: 'openai-compatible', name: 'provider' },
runtimeAdapter: {
kind: 'openai-compatible',
name: 'provider',
runtimeProfile: 'alibaba-token-plan',
},
modelDiscovery: { kind: 'protocol' },
category: 'domestic',
catalogGroup: 'plans',
Expand All @@ -1684,7 +1709,11 @@ const providerRegistry = {
fallbackModels: [...alibabaTokenPlanModelIds],
status: 'ready',
protocol: 'openai',
runtimeAdapter: { kind: 'openai-compatible', name: 'provider' },
runtimeAdapter: {
kind: 'openai-compatible',
name: 'provider',
runtimeProfile: 'alibaba-token-plan',
},
modelDiscovery: { kind: 'protocol' },
category: 'overseas',
catalogGroup: 'plans',
Expand Down
Loading