Compare commits
3 Commits
b92974716f
...
26810d9410
| Author | SHA1 | Date | |
|---|---|---|---|
| 26810d9410 | |||
| b14685d9a5 | |||
| 4dc518a5f4 |
File diff suppressed because it is too large
Load Diff
1036
docs/api_reference/anthropic/create.md
Normal file
1036
docs/api_reference/anthropic/create.md
Normal file
File diff suppressed because it is too large
Load Diff
240
docs/api_reference/anthropic/list.md
Normal file
240
docs/api_reference/anthropic/list.md
Normal file
@@ -0,0 +1,240 @@
|
||||
## List
|
||||
|
||||
`$ ant models list`
|
||||
|
||||
**get** `/v1/models`
|
||||
|
||||
List available models.
|
||||
|
||||
The Models API response can be used to determine which models are available for use in the API. More recently released models are listed first.
|
||||
|
||||
### Parameters
|
||||
|
||||
- `--after-id: optional string`
|
||||
|
||||
Query param: ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object.
|
||||
|
||||
- `--before-id: optional string`
|
||||
|
||||
Query param: ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object.
|
||||
|
||||
- `--limit: optional number`
|
||||
|
||||
Query param: Number of items to return per page.
|
||||
|
||||
Defaults to `20`. Ranges from `1` to `1000`.
|
||||
|
||||
- `--beta: optional array of AnthropicBeta`
|
||||
|
||||
Header param: Optional header to specify the beta version(s) you want to use.
|
||||
|
||||
### Returns
|
||||
|
||||
- `ListResponse_ModelInfo_: object { data, first_id, has_more, last_id }`
|
||||
|
||||
- `data: array of ModelInfo`
|
||||
|
||||
- `id: string`
|
||||
|
||||
Unique model identifier.
|
||||
|
||||
- `capabilities: object { batch, citations, code_execution, 6 more }`
|
||||
|
||||
Model capability information.
|
||||
|
||||
- `batch: object { supported }`
|
||||
|
||||
Whether the model supports the Batch API.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `citations: object { supported }`
|
||||
|
||||
Whether the model supports citation generation.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `code_execution: object { supported }`
|
||||
|
||||
Whether the model supports code execution tools.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `context_management: object { clear_thinking_20251015, clear_tool_uses_20250919, compact_20260112, supported }`
|
||||
|
||||
Context management support and available strategies.
|
||||
|
||||
- `clear_thinking_20251015: object { supported }`
|
||||
|
||||
Indicates whether a capability is supported.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `clear_tool_uses_20250919: object { supported }`
|
||||
|
||||
Indicates whether a capability is supported.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `compact_20260112: object { supported }`
|
||||
|
||||
Indicates whether a capability is supported.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `effort: object { high, low, max, 3 more }`
|
||||
|
||||
Effort (reasoning_effort) support and available levels.
|
||||
|
||||
- `high: object { supported }`
|
||||
|
||||
Whether the model supports high effort level.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `low: object { supported }`
|
||||
|
||||
Whether the model supports low effort level.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `max: object { supported }`
|
||||
|
||||
Whether the model supports max effort level.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `medium: object { supported }`
|
||||
|
||||
Whether the model supports medium effort level.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `xhigh: object { supported }`
|
||||
|
||||
Indicates whether a capability is supported.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `image_input: object { supported }`
|
||||
|
||||
Whether the model accepts image content blocks.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `pdf_input: object { supported }`
|
||||
|
||||
Whether the model accepts PDF content blocks.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `structured_outputs: object { supported }`
|
||||
|
||||
Whether the model supports structured output / JSON mode / strict tool schemas.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `thinking: object { supported, types }`
|
||||
|
||||
Thinking capability and supported type configurations.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `types: object { adaptive, enabled }`
|
||||
|
||||
Supported thinking type configurations.
|
||||
|
||||
- `adaptive: object { supported }`
|
||||
|
||||
Whether the model supports thinking with type 'adaptive' (auto).
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `enabled: object { supported }`
|
||||
|
||||
Whether the model supports thinking with type 'enabled'.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `created_at: string`
|
||||
|
||||
RFC 3339 datetime string representing the time at which the model was released. May be set to an epoch value if the release date is unknown.
|
||||
|
||||
- `display_name: string`
|
||||
|
||||
A human-readable name for the model.
|
||||
|
||||
- `max_input_tokens: number`
|
||||
|
||||
Maximum input context window size in tokens for this model.
|
||||
|
||||
- `max_tokens: number`
|
||||
|
||||
Maximum value for the `max_tokens` parameter when using this model.
|
||||
|
||||
- `type: "model"`
|
||||
|
||||
Object type.
|
||||
|
||||
For Models, this is always `"model"`.
|
||||
|
||||
- `first_id: string`
|
||||
|
||||
First ID in the `data` list. Can be used as the `before_id` for the previous page.
|
||||
|
||||
- `has_more: boolean`
|
||||
|
||||
Indicates if there are more results in the requested page direction.
|
||||
|
||||
- `last_id: string`
|
||||
|
||||
Last ID in the `data` list. Can be used as the `after_id` for the next page.
|
||||
|
||||
### Example
|
||||
|
||||
```cli
|
||||
ant models list \
|
||||
--api-key my-anthropic-api-key
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,291 +0,0 @@
|
||||
## List
|
||||
|
||||
**get** `/v1/models`
|
||||
|
||||
List available models.
|
||||
|
||||
The Models API response can be used to determine which models are available for use in the API. More recently released models are listed first.
|
||||
|
||||
### Query Parameters
|
||||
|
||||
- `after_id: optional string`
|
||||
|
||||
ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object.
|
||||
|
||||
- `before_id: optional string`
|
||||
|
||||
ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object.
|
||||
|
||||
- `limit: optional number`
|
||||
|
||||
Number of items to return per page.
|
||||
|
||||
Defaults to `20`. Ranges from `1` to `1000`.
|
||||
|
||||
### Header Parameters
|
||||
|
||||
- `"anthropic-beta": optional array of AnthropicBeta`
|
||||
|
||||
Optional header to specify the beta version(s) you want to use.
|
||||
|
||||
- `UnionMember0 = string`
|
||||
|
||||
- `UnionMember1 = "message-batches-2024-09-24" or "prompt-caching-2024-07-31" or "computer-use-2024-10-22" or 20 more`
|
||||
|
||||
- `"message-batches-2024-09-24"`
|
||||
|
||||
- `"prompt-caching-2024-07-31"`
|
||||
|
||||
- `"computer-use-2024-10-22"`
|
||||
|
||||
- `"computer-use-2025-01-24"`
|
||||
|
||||
- `"pdfs-2024-09-25"`
|
||||
|
||||
- `"token-counting-2024-11-01"`
|
||||
|
||||
- `"token-efficient-tools-2025-02-19"`
|
||||
|
||||
- `"output-128k-2025-02-19"`
|
||||
|
||||
- `"files-api-2025-04-14"`
|
||||
|
||||
- `"mcp-client-2025-04-04"`
|
||||
|
||||
- `"mcp-client-2025-11-20"`
|
||||
|
||||
- `"dev-full-thinking-2025-05-14"`
|
||||
|
||||
- `"interleaved-thinking-2025-05-14"`
|
||||
|
||||
- `"code-execution-2025-05-22"`
|
||||
|
||||
- `"extended-cache-ttl-2025-04-11"`
|
||||
|
||||
- `"context-1m-2025-08-07"`
|
||||
|
||||
- `"context-management-2025-06-27"`
|
||||
|
||||
- `"model-context-window-exceeded-2025-08-26"`
|
||||
|
||||
- `"skills-2025-10-02"`
|
||||
|
||||
- `"fast-mode-2026-02-01"`
|
||||
|
||||
- `"output-300k-2026-03-24"`
|
||||
|
||||
- `"advisor-tool-2026-03-01"`
|
||||
|
||||
- `"user-profiles-2026-03-24"`
|
||||
|
||||
### Returns
|
||||
|
||||
- `data: array of ModelInfo`
|
||||
|
||||
- `id: string`
|
||||
|
||||
Unique model identifier.
|
||||
|
||||
- `capabilities: ModelCapabilities`
|
||||
|
||||
Model capability information.
|
||||
|
||||
- `batch: CapabilitySupport`
|
||||
|
||||
Whether the model supports the Batch API.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `citations: CapabilitySupport`
|
||||
|
||||
Whether the model supports citation generation.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `code_execution: CapabilitySupport`
|
||||
|
||||
Whether the model supports code execution tools.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `context_management: ContextManagementCapability`
|
||||
|
||||
Context management support and available strategies.
|
||||
|
||||
- `clear_thinking_20251015: CapabilitySupport`
|
||||
|
||||
Indicates whether a capability is supported.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `clear_tool_uses_20250919: CapabilitySupport`
|
||||
|
||||
Indicates whether a capability is supported.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `compact_20260112: CapabilitySupport`
|
||||
|
||||
Indicates whether a capability is supported.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `effort: EffortCapability`
|
||||
|
||||
Effort (reasoning_effort) support and available levels.
|
||||
|
||||
- `high: CapabilitySupport`
|
||||
|
||||
Whether the model supports high effort level.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `low: CapabilitySupport`
|
||||
|
||||
Whether the model supports low effort level.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `max: CapabilitySupport`
|
||||
|
||||
Whether the model supports max effort level.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `medium: CapabilitySupport`
|
||||
|
||||
Whether the model supports medium effort level.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `xhigh: CapabilitySupport`
|
||||
|
||||
Indicates whether a capability is supported.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `image_input: CapabilitySupport`
|
||||
|
||||
Whether the model accepts image content blocks.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `pdf_input: CapabilitySupport`
|
||||
|
||||
Whether the model accepts PDF content blocks.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `structured_outputs: CapabilitySupport`
|
||||
|
||||
Whether the model supports structured output / JSON mode / strict tool schemas.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `thinking: ThinkingCapability`
|
||||
|
||||
Thinking capability and supported type configurations.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `types: ThinkingTypes`
|
||||
|
||||
Supported thinking type configurations.
|
||||
|
||||
- `adaptive: CapabilitySupport`
|
||||
|
||||
Whether the model supports thinking with type 'adaptive' (auto).
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `enabled: CapabilitySupport`
|
||||
|
||||
Whether the model supports thinking with type 'enabled'.
|
||||
|
||||
- `supported: boolean`
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `created_at: string`
|
||||
|
||||
RFC 3339 datetime string representing the time at which the model was released. May be set to an epoch value if the release date is unknown.
|
||||
|
||||
- `display_name: string`
|
||||
|
||||
A human-readable name for the model.
|
||||
|
||||
- `max_input_tokens: number`
|
||||
|
||||
Maximum input context window size in tokens for this model.
|
||||
|
||||
- `max_tokens: number`
|
||||
|
||||
Maximum value for the `max_tokens` parameter when using this model.
|
||||
|
||||
- `type: "model"`
|
||||
|
||||
Object type.
|
||||
|
||||
For Models, this is always `"model"`.
|
||||
|
||||
- `"model"`
|
||||
|
||||
- `first_id: string`
|
||||
|
||||
First ID in the `data` list. Can be used as the `before_id` for the previous page.
|
||||
|
||||
- `has_more: boolean`
|
||||
|
||||
Indicates if there are more results in the requested page direction.
|
||||
|
||||
- `last_id: string`
|
||||
|
||||
Last ID in the `data` list. Can be used as the `after_id` for the next page.
|
||||
|
||||
### Example
|
||||
|
||||
```http
|
||||
curl https://api.anthropic.com/v1/models \
|
||||
-H 'anthropic-version: 2023-06-01' \
|
||||
-H "X-Api-Key: $ANTHROPIC_API_KEY"
|
||||
```
|
||||
@@ -1,86 +1,36 @@
|
||||
## Retrieve
|
||||
|
||||
`$ ant models retrieve`
|
||||
|
||||
**get** `/v1/models/{model_id}`
|
||||
|
||||
Get a specific model.
|
||||
|
||||
The Models API response can be used to determine information about a specific model or resolve a model alias to a model ID.
|
||||
|
||||
### Path Parameters
|
||||
### Parameters
|
||||
|
||||
- `model_id: string`
|
||||
- `--model-id: string`
|
||||
|
||||
Model identifier or alias.
|
||||
|
||||
### Header Parameters
|
||||
|
||||
- `"anthropic-beta": optional array of AnthropicBeta`
|
||||
- `--beta: optional array of AnthropicBeta`
|
||||
|
||||
Optional header to specify the beta version(s) you want to use.
|
||||
|
||||
- `UnionMember0 = string`
|
||||
|
||||
- `UnionMember1 = "message-batches-2024-09-24" or "prompt-caching-2024-07-31" or "computer-use-2024-10-22" or 20 more`
|
||||
|
||||
- `"message-batches-2024-09-24"`
|
||||
|
||||
- `"prompt-caching-2024-07-31"`
|
||||
|
||||
- `"computer-use-2024-10-22"`
|
||||
|
||||
- `"computer-use-2025-01-24"`
|
||||
|
||||
- `"pdfs-2024-09-25"`
|
||||
|
||||
- `"token-counting-2024-11-01"`
|
||||
|
||||
- `"token-efficient-tools-2025-02-19"`
|
||||
|
||||
- `"output-128k-2025-02-19"`
|
||||
|
||||
- `"files-api-2025-04-14"`
|
||||
|
||||
- `"mcp-client-2025-04-04"`
|
||||
|
||||
- `"mcp-client-2025-11-20"`
|
||||
|
||||
- `"dev-full-thinking-2025-05-14"`
|
||||
|
||||
- `"interleaved-thinking-2025-05-14"`
|
||||
|
||||
- `"code-execution-2025-05-22"`
|
||||
|
||||
- `"extended-cache-ttl-2025-04-11"`
|
||||
|
||||
- `"context-1m-2025-08-07"`
|
||||
|
||||
- `"context-management-2025-06-27"`
|
||||
|
||||
- `"model-context-window-exceeded-2025-08-26"`
|
||||
|
||||
- `"skills-2025-10-02"`
|
||||
|
||||
- `"fast-mode-2026-02-01"`
|
||||
|
||||
- `"output-300k-2026-03-24"`
|
||||
|
||||
- `"advisor-tool-2026-03-01"`
|
||||
|
||||
- `"user-profiles-2026-03-24"`
|
||||
|
||||
### Returns
|
||||
|
||||
- `ModelInfo = object { id, capabilities, created_at, 4 more }`
|
||||
- `model_info: object { id, capabilities, created_at, 4 more }`
|
||||
|
||||
- `id: string`
|
||||
|
||||
Unique model identifier.
|
||||
|
||||
- `capabilities: ModelCapabilities`
|
||||
- `capabilities: object { batch, citations, code_execution, 6 more }`
|
||||
|
||||
Model capability information.
|
||||
|
||||
- `batch: CapabilitySupport`
|
||||
- `batch: object { supported }`
|
||||
|
||||
Whether the model supports the Batch API.
|
||||
|
||||
@@ -88,7 +38,7 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `citations: CapabilitySupport`
|
||||
- `citations: object { supported }`
|
||||
|
||||
Whether the model supports citation generation.
|
||||
|
||||
@@ -96,7 +46,7 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `code_execution: CapabilitySupport`
|
||||
- `code_execution: object { supported }`
|
||||
|
||||
Whether the model supports code execution tools.
|
||||
|
||||
@@ -104,11 +54,11 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `context_management: ContextManagementCapability`
|
||||
- `context_management: object { clear_thinking_20251015, clear_tool_uses_20250919, compact_20260112, supported }`
|
||||
|
||||
Context management support and available strategies.
|
||||
|
||||
- `clear_thinking_20251015: CapabilitySupport`
|
||||
- `clear_thinking_20251015: object { supported }`
|
||||
|
||||
Indicates whether a capability is supported.
|
||||
|
||||
@@ -116,7 +66,7 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `clear_tool_uses_20250919: CapabilitySupport`
|
||||
- `clear_tool_uses_20250919: object { supported }`
|
||||
|
||||
Indicates whether a capability is supported.
|
||||
|
||||
@@ -124,7 +74,7 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `compact_20260112: CapabilitySupport`
|
||||
- `compact_20260112: object { supported }`
|
||||
|
||||
Indicates whether a capability is supported.
|
||||
|
||||
@@ -136,11 +86,11 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `effort: EffortCapability`
|
||||
- `effort: object { high, low, max, 3 more }`
|
||||
|
||||
Effort (reasoning_effort) support and available levels.
|
||||
|
||||
- `high: CapabilitySupport`
|
||||
- `high: object { supported }`
|
||||
|
||||
Whether the model supports high effort level.
|
||||
|
||||
@@ -148,7 +98,7 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `low: CapabilitySupport`
|
||||
- `low: object { supported }`
|
||||
|
||||
Whether the model supports low effort level.
|
||||
|
||||
@@ -156,7 +106,7 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `max: CapabilitySupport`
|
||||
- `max: object { supported }`
|
||||
|
||||
Whether the model supports max effort level.
|
||||
|
||||
@@ -164,7 +114,7 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `medium: CapabilitySupport`
|
||||
- `medium: object { supported }`
|
||||
|
||||
Whether the model supports medium effort level.
|
||||
|
||||
@@ -176,7 +126,7 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `xhigh: CapabilitySupport`
|
||||
- `xhigh: object { supported }`
|
||||
|
||||
Indicates whether a capability is supported.
|
||||
|
||||
@@ -184,7 +134,7 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `image_input: CapabilitySupport`
|
||||
- `image_input: object { supported }`
|
||||
|
||||
Whether the model accepts image content blocks.
|
||||
|
||||
@@ -192,7 +142,7 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `pdf_input: CapabilitySupport`
|
||||
- `pdf_input: object { supported }`
|
||||
|
||||
Whether the model accepts PDF content blocks.
|
||||
|
||||
@@ -200,7 +150,7 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `structured_outputs: CapabilitySupport`
|
||||
- `structured_outputs: object { supported }`
|
||||
|
||||
Whether the model supports structured output / JSON mode / strict tool schemas.
|
||||
|
||||
@@ -208,7 +158,7 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `thinking: ThinkingCapability`
|
||||
- `thinking: object { supported, types }`
|
||||
|
||||
Thinking capability and supported type configurations.
|
||||
|
||||
@@ -216,11 +166,11 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `types: ThinkingTypes`
|
||||
- `types: object { adaptive, enabled }`
|
||||
|
||||
Supported thinking type configurations.
|
||||
|
||||
- `adaptive: CapabilitySupport`
|
||||
- `adaptive: object { supported }`
|
||||
|
||||
Whether the model supports thinking with type 'adaptive' (auto).
|
||||
|
||||
@@ -228,7 +178,7 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
Whether this capability is supported by the model.
|
||||
|
||||
- `enabled: CapabilitySupport`
|
||||
- `enabled: object { supported }`
|
||||
|
||||
Whether the model supports thinking with type 'enabled'.
|
||||
|
||||
@@ -258,12 +208,10 @@ The Models API response can be used to determine information about a specific mo
|
||||
|
||||
For Models, this is always `"model"`.
|
||||
|
||||
- `"model"`
|
||||
|
||||
### Example
|
||||
|
||||
```http
|
||||
curl https://api.anthropic.com/v1/models/$MODEL_ID \
|
||||
-H 'anthropic-version: 2023-06-01' \
|
||||
-H "X-Api-Key: $ANTHROPIC_API_KEY"
|
||||
```cli
|
||||
ant models retrieve \
|
||||
--api-key my-anthropic-api-key \
|
||||
--model-id model_id
|
||||
```
|
||||
175
docs/api_reference/openai/Create embeddings.md
Normal file
175
docs/api_reference/openai/Create embeddings.md
Normal file
@@ -0,0 +1,175 @@
|
||||
## Create embeddings
|
||||
|
||||
**post** `/embeddings`
|
||||
|
||||
Creates an embedding vector representing the input text.
|
||||
|
||||
### Body Parameters
|
||||
|
||||
- `input: string or array of string or array of number or array of array of number`
|
||||
|
||||
Input text to embed, encoded as a string or array of tokens. To embed multiple inputs in a single request, pass an array of strings or array of token arrays. The input must not exceed the max input tokens for the model (8192 tokens for all embedding models), cannot be an empty string, and any array must be 2048 dimensions or less. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens. In addition to the per-input token limit, all embedding models enforce a maximum of 300,000 tokens summed across all inputs in a single request.
|
||||
|
||||
- `String = string`
|
||||
|
||||
The string that will be turned into an embedding.
|
||||
|
||||
- `Array = array of string`
|
||||
|
||||
The array of strings that will be turned into an embedding.
|
||||
|
||||
- `Array = array of number`
|
||||
|
||||
The array of integers that will be turned into an embedding.
|
||||
|
||||
- `Array = array of array of number`
|
||||
|
||||
The array of arrays containing integers that will be turned into an embedding.
|
||||
|
||||
- `model: string or EmbeddingModel`
|
||||
|
||||
ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models) for descriptions of them.
|
||||
|
||||
- `string`
|
||||
|
||||
- `EmbeddingModel = "text-embedding-ada-002" or "text-embedding-3-small" or "text-embedding-3-large"`
|
||||
|
||||
- `"text-embedding-ada-002"`
|
||||
|
||||
- `"text-embedding-3-small"`
|
||||
|
||||
- `"text-embedding-3-large"`
|
||||
|
||||
- `dimensions: optional number`
|
||||
|
||||
The number of dimensions the resulting output embeddings should have. Only supported in `text-embedding-3` and later models.
|
||||
|
||||
- `encoding_format: optional "float" or "base64"`
|
||||
|
||||
The format to return the embeddings in. Can be either `float` or [`base64`](https://pypi.org/project/pybase64/).
|
||||
|
||||
- `"float"`
|
||||
|
||||
- `"base64"`
|
||||
|
||||
- `user: optional string`
|
||||
|
||||
A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices#end-user-ids).
|
||||
|
||||
### Returns
|
||||
|
||||
- `CreateEmbeddingResponse object { data, model, object, usage }`
|
||||
|
||||
- `data: array of Embedding`
|
||||
|
||||
The list of embeddings generated by the model.
|
||||
|
||||
- `embedding: array of number`
|
||||
|
||||
The embedding vector, which is a list of floats. The length of vector depends on the model as listed in the [embedding guide](/docs/guides/embeddings).
|
||||
|
||||
- `index: number`
|
||||
|
||||
The index of the embedding in the list of embeddings.
|
||||
|
||||
- `object: "embedding"`
|
||||
|
||||
The object type, which is always "embedding".
|
||||
|
||||
- `"embedding"`
|
||||
|
||||
- `model: string`
|
||||
|
||||
The name of the model used to generate the embedding.
|
||||
|
||||
- `object: "list"`
|
||||
|
||||
The object type, which is always "list".
|
||||
|
||||
- `"list"`
|
||||
|
||||
- `usage: object { prompt_tokens, total_tokens }`
|
||||
|
||||
The usage information for the request.
|
||||
|
||||
- `prompt_tokens: number`
|
||||
|
||||
The number of tokens used by the prompt.
|
||||
|
||||
- `total_tokens: number`
|
||||
|
||||
The total number of tokens used by the request.
|
||||
|
||||
### Example
|
||||
|
||||
```http
|
||||
curl https://api.openai.com/v1/embeddings \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
||||
-d '{
|
||||
"input": "The quick brown fox jumped over the lazy dog",
|
||||
"model": "text-embedding-3-small",
|
||||
"encoding_format": "float",
|
||||
"user": "user-1234"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"embedding": [
|
||||
0
|
||||
],
|
||||
"index": 0,
|
||||
"object": "embedding"
|
||||
}
|
||||
],
|
||||
"model": "model",
|
||||
"object": "list",
|
||||
"usage": {
|
||||
"prompt_tokens": 0,
|
||||
"total_tokens": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```http
|
||||
curl https://api.openai.com/v1/embeddings \
|
||||
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"input": "The food was delicious and the waiter...",
|
||||
"model": "text-embedding-ada-002",
|
||||
"encoding_format": "float"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"object": "embedding",
|
||||
"embedding": [
|
||||
0.0023064255,
|
||||
-0.009327292,
|
||||
.... (1536 floats total for ada-002)
|
||||
-0.0028842222,
|
||||
],
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
"model": "text-embedding-ada-002",
|
||||
"usage": {
|
||||
"prompt_tokens": 8,
|
||||
"total_tokens": 8
|
||||
}
|
||||
}
|
||||
```
|
||||
367
docs/api_reference/openai/Create image.md
Normal file
367
docs/api_reference/openai/Create image.md
Normal file
@@ -0,0 +1,367 @@
|
||||
## Create image
|
||||
|
||||
**post** `/images/generations`
|
||||
|
||||
Creates an image given a prompt. [Learn more](/docs/guides/images).
|
||||
|
||||
### Body Parameters
|
||||
|
||||
- `prompt: string`
|
||||
|
||||
A text description of the desired image(s). The maximum length is 32000 characters for the GPT image models, 1000 characters for `dall-e-2` and 4000 characters for `dall-e-3`.
|
||||
|
||||
- `background: optional "transparent" or "opaque" or "auto"`
|
||||
|
||||
Allows to set transparency for the background of the generated image(s).
|
||||
This parameter is only supported for the GPT image models. Must be one of
|
||||
`transparent`, `opaque` or `auto` (default value). When `auto` is used, the
|
||||
model will automatically determine the best background for the image.
|
||||
|
||||
If `transparent`, the output format needs to support transparency, so it
|
||||
should be set to either `png` (default value) or `webp`.
|
||||
|
||||
- `"transparent"`
|
||||
|
||||
- `"opaque"`
|
||||
|
||||
- `"auto"`
|
||||
|
||||
- `model: optional string or ImageModel`
|
||||
|
||||
The model to use for image generation. One of `dall-e-2`, `dall-e-3`, or a GPT image model (`gpt-image-1`, `gpt-image-1-mini`, `gpt-image-1.5`). Defaults to `dall-e-2` unless a parameter specific to the GPT image models is used.
|
||||
|
||||
- `string`
|
||||
|
||||
- `ImageModel = "gpt-image-1.5" or "dall-e-2" or "dall-e-3" or 2 more`
|
||||
|
||||
- `"gpt-image-1.5"`
|
||||
|
||||
- `"dall-e-2"`
|
||||
|
||||
- `"dall-e-3"`
|
||||
|
||||
- `"gpt-image-1"`
|
||||
|
||||
- `"gpt-image-1-mini"`
|
||||
|
||||
- `moderation: optional "low" or "auto"`
|
||||
|
||||
Control the content-moderation level for images generated by the GPT image models. Must be either `low` for less restrictive filtering or `auto` (default value).
|
||||
|
||||
- `"low"`
|
||||
|
||||
- `"auto"`
|
||||
|
||||
- `n: optional number`
|
||||
|
||||
The number of images to generate. Must be between 1 and 10. For `dall-e-3`, only `n=1` is supported.
|
||||
|
||||
- `output_compression: optional number`
|
||||
|
||||
The compression level (0-100%) for the generated images. This parameter is only supported for the GPT image models with the `webp` or `jpeg` output formats, and defaults to 100.
|
||||
|
||||
- `output_format: optional "png" or "jpeg" or "webp"`
|
||||
|
||||
The format in which the generated images are returned. This parameter is only supported for the GPT image models. Must be one of `png`, `jpeg`, or `webp`.
|
||||
|
||||
- `"png"`
|
||||
|
||||
- `"jpeg"`
|
||||
|
||||
- `"webp"`
|
||||
|
||||
- `partial_images: optional number`
|
||||
|
||||
The number of partial images to generate. This parameter is used for
|
||||
streaming responses that return partial images. Value must be between 0 and 3.
|
||||
When set to 0, the response will be a single image sent in one streaming event.
|
||||
|
||||
Note that the final image may be sent before the full number of partial images
|
||||
are generated if the full image is generated more quickly.
|
||||
|
||||
- `quality: optional "standard" or "hd" or "low" or 3 more`
|
||||
|
||||
The quality of the image that will be generated.
|
||||
|
||||
- `auto` (default value) will automatically select the best quality for the given model.
|
||||
- `high`, `medium` and `low` are supported for the GPT image models.
|
||||
- `hd` and `standard` are supported for `dall-e-3`.
|
||||
- `standard` is the only option for `dall-e-2`.
|
||||
|
||||
- `"standard"`
|
||||
|
||||
- `"hd"`
|
||||
|
||||
- `"low"`
|
||||
|
||||
- `"medium"`
|
||||
|
||||
- `"high"`
|
||||
|
||||
- `"auto"`
|
||||
|
||||
- `response_format: optional "url" or "b64_json"`
|
||||
|
||||
The format in which generated images with `dall-e-2` and `dall-e-3` are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. This parameter isn't supported for the GPT image models, which always return base64-encoded images.
|
||||
|
||||
- `"url"`
|
||||
|
||||
- `"b64_json"`
|
||||
|
||||
- `size: optional "auto" or "1024x1024" or "1536x1024" or 5 more`
|
||||
|
||||
The size of the generated images. Must be one of `1024x1024`, `1536x1024` (landscape), `1024x1536` (portrait), or `auto` (default value) for the GPT image models, one of `256x256`, `512x512`, or `1024x1024` for `dall-e-2`, and one of `1024x1024`, `1792x1024`, or `1024x1792` for `dall-e-3`.
|
||||
|
||||
- `"auto"`
|
||||
|
||||
- `"1024x1024"`
|
||||
|
||||
- `"1536x1024"`
|
||||
|
||||
- `"1024x1536"`
|
||||
|
||||
- `"256x256"`
|
||||
|
||||
- `"512x512"`
|
||||
|
||||
- `"1792x1024"`
|
||||
|
||||
- `"1024x1792"`
|
||||
|
||||
- `stream: optional boolean`
|
||||
|
||||
Generate the image in streaming mode. Defaults to `false`. See the
|
||||
[Image generation guide](/docs/guides/image-generation) for more information.
|
||||
This parameter is only supported for the GPT image models.
|
||||
|
||||
- `style: optional "vivid" or "natural"`
|
||||
|
||||
The style of the generated images. This parameter is only supported for `dall-e-3`. Must be one of `vivid` or `natural`. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images.
|
||||
|
||||
- `"vivid"`
|
||||
|
||||
- `"natural"`
|
||||
|
||||
- `user: optional string`
|
||||
|
||||
A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices#end-user-ids).
|
||||
|
||||
### Returns
|
||||
|
||||
- `ImagesResponse object { created, background, data, 4 more }`
|
||||
|
||||
The response from the image generation endpoint.
|
||||
|
||||
- `created: number`
|
||||
|
||||
The Unix timestamp (in seconds) of when the image was created.
|
||||
|
||||
- `background: optional "transparent" or "opaque"`
|
||||
|
||||
The background parameter used for the image generation. Either `transparent` or `opaque`.
|
||||
|
||||
- `"transparent"`
|
||||
|
||||
- `"opaque"`
|
||||
|
||||
- `data: optional array of Image`
|
||||
|
||||
The list of generated images.
|
||||
|
||||
- `b64_json: optional string`
|
||||
|
||||
The base64-encoded JSON of the generated image. Returned by default for the GPT image models, and only present if `response_format` is set to `b64_json` for `dall-e-2` and `dall-e-3`.
|
||||
|
||||
- `revised_prompt: optional string`
|
||||
|
||||
For `dall-e-3` only, the revised prompt that was used to generate the image.
|
||||
|
||||
- `url: optional string`
|
||||
|
||||
When using `dall-e-2` or `dall-e-3`, the URL of the generated image if `response_format` is set to `url` (default value). Unsupported for the GPT image models.
|
||||
|
||||
- `output_format: optional "png" or "webp" or "jpeg"`
|
||||
|
||||
The output format of the image generation. Either `png`, `webp`, or `jpeg`.
|
||||
|
||||
- `"png"`
|
||||
|
||||
- `"webp"`
|
||||
|
||||
- `"jpeg"`
|
||||
|
||||
- `quality: optional "low" or "medium" or "high"`
|
||||
|
||||
The quality of the image generated. Either `low`, `medium`, or `high`.
|
||||
|
||||
- `"low"`
|
||||
|
||||
- `"medium"`
|
||||
|
||||
- `"high"`
|
||||
|
||||
- `size: optional "1024x1024" or "1024x1536" or "1536x1024"`
|
||||
|
||||
The size of the image generated. Either `1024x1024`, `1024x1536`, or `1536x1024`.
|
||||
|
||||
- `"1024x1024"`
|
||||
|
||||
- `"1024x1536"`
|
||||
|
||||
- `"1536x1024"`
|
||||
|
||||
- `usage: optional object { input_tokens, input_tokens_details, output_tokens, 2 more }`
|
||||
|
||||
For `gpt-image-1` only, the token usage information for the image generation.
|
||||
|
||||
- `input_tokens: number`
|
||||
|
||||
The number of tokens (images and text) in the input prompt.
|
||||
|
||||
- `input_tokens_details: object { image_tokens, text_tokens }`
|
||||
|
||||
The input tokens detailed information for the image generation.
|
||||
|
||||
- `image_tokens: number`
|
||||
|
||||
The number of image tokens in the input prompt.
|
||||
|
||||
- `text_tokens: number`
|
||||
|
||||
The number of text tokens in the input prompt.
|
||||
|
||||
- `output_tokens: number`
|
||||
|
||||
The number of output tokens generated by the model.
|
||||
|
||||
- `total_tokens: number`
|
||||
|
||||
The total number of tokens (images and text) used for the image generation.
|
||||
|
||||
- `output_tokens_details: optional object { image_tokens, text_tokens }`
|
||||
|
||||
The output token details for the image generation.
|
||||
|
||||
- `image_tokens: number`
|
||||
|
||||
The number of image output tokens generated by the model.
|
||||
|
||||
- `text_tokens: number`
|
||||
|
||||
The number of text output tokens generated by the model.
|
||||
|
||||
### Example
|
||||
|
||||
```http
|
||||
curl https://api.openai.com/v1/images/generations \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
||||
-d '{
|
||||
"prompt": "A cute baby sea otter",
|
||||
"background": "transparent",
|
||||
"moderation": "low",
|
||||
"n": 1,
|
||||
"output_compression": 100,
|
||||
"output_format": "png",
|
||||
"partial_images": 1,
|
||||
"quality": "medium",
|
||||
"response_format": "url",
|
||||
"size": "1024x1024",
|
||||
"style": "vivid",
|
||||
"user": "user-1234"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"created": 0,
|
||||
"background": "transparent",
|
||||
"data": [
|
||||
{
|
||||
"b64_json": "b64_json",
|
||||
"revised_prompt": "revised_prompt",
|
||||
"url": "url"
|
||||
}
|
||||
],
|
||||
"output_format": "png",
|
||||
"quality": "low",
|
||||
"size": "1024x1024",
|
||||
"usage": {
|
||||
"input_tokens": 0,
|
||||
"input_tokens_details": {
|
||||
"image_tokens": 0,
|
||||
"text_tokens": 0
|
||||
},
|
||||
"output_tokens": 0,
|
||||
"total_tokens": 0,
|
||||
"output_tokens_details": {
|
||||
"image_tokens": 0,
|
||||
"text_tokens": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Generate image
|
||||
|
||||
```http
|
||||
curl https://api.openai.com/v1/images/generations \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
||||
-d '{
|
||||
"model": "gpt-image-1.5",
|
||||
"prompt": "A cute baby sea otter",
|
||||
"n": 1,
|
||||
"size": "1024x1024"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"created": 1713833628,
|
||||
"data": [
|
||||
{
|
||||
"b64_json": "..."
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"total_tokens": 100,
|
||||
"input_tokens": 50,
|
||||
"output_tokens": 50,
|
||||
"input_tokens_details": {
|
||||
"text_tokens": 10,
|
||||
"image_tokens": 40
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming
|
||||
|
||||
```http
|
||||
curl https://api.openai.com/v1/images/generations \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
||||
-d '{
|
||||
"model": "gpt-image-1.5",
|
||||
"prompt": "A cute baby sea otter",
|
||||
"n": 1,
|
||||
"size": "1024x1024",
|
||||
"stream": true
|
||||
}' \
|
||||
--no-buffer
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
event: image_generation.partial_image
|
||||
data: {"type":"image_generation.partial_image","b64_json":"...","partial_image_index":0}
|
||||
|
||||
event: image_generation.completed
|
||||
data: {"type":"image_generation.completed","b64_json":"...","usage":{"total_tokens":100,"input_tokens":50,"output_tokens":50,"input_tokens_details":{"text_tokens":10,"image_tokens":40}}}
|
||||
```
|
||||
69
docs/api_reference/openai/Retrieve model.md
Normal file
69
docs/api_reference/openai/Retrieve model.md
Normal file
@@ -0,0 +1,69 @@
|
||||
## Retrieve model
|
||||
|
||||
**get** `/models/{model}`
|
||||
|
||||
Retrieves a model instance, providing basic information about the model such as the owner and permissioning.
|
||||
|
||||
### Path Parameters
|
||||
|
||||
- `model: string`
|
||||
|
||||
### Returns
|
||||
|
||||
- `Model object { id, created, object, owned_by }`
|
||||
|
||||
Describes an OpenAI model offering that can be used with the API.
|
||||
|
||||
- `id: string`
|
||||
|
||||
The model identifier, which can be referenced in the API endpoints.
|
||||
|
||||
- `created: number`
|
||||
|
||||
The Unix timestamp (in seconds) when the model was created.
|
||||
|
||||
- `object: "model"`
|
||||
|
||||
The object type, which is always "model".
|
||||
|
||||
- `"model"`
|
||||
|
||||
- `owned_by: string`
|
||||
|
||||
The organization that owns the model.
|
||||
|
||||
### Example
|
||||
|
||||
```http
|
||||
curl https://api.openai.com/v1/models/$MODEL \
|
||||
-H "Authorization: Bearer $OPENAI_API_KEY"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "id",
|
||||
"created": 0,
|
||||
"object": "model",
|
||||
"owned_by": "owned_by"
|
||||
}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```http
|
||||
curl https://api.openai.com/v1/models/VAR_chat_model_id \
|
||||
-H "Authorization: Bearer $OPENAI_API_KEY"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "VAR_chat_model_id",
|
||||
"object": "model",
|
||||
"created": 1686935002,
|
||||
"owned_by": "openai"
|
||||
}
|
||||
```
|
||||
248
docs/api_reference/openai/chat/Streaming events.md
Normal file
248
docs/api_reference/openai/chat/Streaming events.md
Normal file
@@ -0,0 +1,248 @@
|
||||
## Streaming events
|
||||
|
||||
Stream Chat Completions in real time. Receive chunks of completions returned from the model using server-sent events. [Learn more](https://developers.openai.com/docs/guides/streaming-responses?api-mode=chat).
|
||||
|
||||
Represents a streamed chunk of a chat completion response returned by the model, based on the provided input. [Learn more](https://developers.openai.com/docs/guides/streaming-responses).
|
||||
|
||||
id: string
|
||||
|
||||
A unique identifier for the chat completion. Each chunk has the same ID.
|
||||
|
||||
choices: array of object { delta, finish\_reason, index, logprobs }
|
||||
|
||||
A list of chat completion choices. Can contain more than one elements if `n` is greater than 1. Can also be empty for the last chunk if you set `stream_options: {"include_usage": true}`.
|
||||
|
||||
delta: object { content, function\_call, refusal, 2 more }
|
||||
|
||||
A chat completion delta generated by streamed model responses.
|
||||
|
||||
content: optional string
|
||||
|
||||
The contents of the chunk message.
|
||||
|
||||
Deprecatedfunction\_call: optional object { arguments, name }
|
||||
|
||||
Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model.
|
||||
|
||||
arguments: optional string
|
||||
|
||||
The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function.
|
||||
|
||||
name: optional string
|
||||
|
||||
The name of the function to call.
|
||||
|
||||
refusal: optional string
|
||||
|
||||
The refusal message generated by the model.
|
||||
|
||||
role: optional "developer" or "system" or "user" or 2 more
|
||||
|
||||
The role of the author of this message.
|
||||
|
||||
One of the following:
|
||||
|
||||
"developer"
|
||||
|
||||
"system"
|
||||
|
||||
"user"
|
||||
|
||||
"assistant"
|
||||
|
||||
"tool"
|
||||
|
||||
tool\_calls: optional array of object { index, id, function, type }
|
||||
|
||||
index: number
|
||||
|
||||
id: optional string
|
||||
|
||||
The ID of the tool call.
|
||||
|
||||
function: optional object { arguments, name }
|
||||
|
||||
arguments: optional string
|
||||
|
||||
The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function.
|
||||
|
||||
name: optional string
|
||||
|
||||
The name of the function to call.
|
||||
|
||||
type: optional "function"
|
||||
|
||||
The type of the tool. Currently, only `function` is supported.
|
||||
|
||||
finish\_reason: "stop" or "length" or "tool\_calls" or 2 more
|
||||
|
||||
The reason the model stopped generating tokens. This will be `stop` if the model hit a natural stop point or a provided stop sequence, `length` if the maximum number of tokens specified in the request was reached, `content_filter` if content was omitted due to a flag from our content filters, `tool_calls` if the model called a tool, or `function_call` (deprecated) if the model called a function.
|
||||
|
||||
One of the following:
|
||||
|
||||
"stop"
|
||||
|
||||
"length"
|
||||
|
||||
"tool\_calls"
|
||||
|
||||
"content\_filter"
|
||||
|
||||
"function\_call"
|
||||
|
||||
index: number
|
||||
|
||||
The index of the choice in the list of choices.
|
||||
|
||||
logprobs: optional object { content, refusal }
|
||||
|
||||
Log probability information for the choice.
|
||||
|
||||
A list of message content tokens with log probability information.
|
||||
|
||||
token: string
|
||||
|
||||
The token.
|
||||
|
||||
bytes: array of number
|
||||
|
||||
A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token.
|
||||
|
||||
logprob: number
|
||||
|
||||
The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value `-9999.0` is used to signify that the token is very unlikely.
|
||||
|
||||
top\_logprobs: array of object { token, bytes, logprob }
|
||||
|
||||
List of the most likely tokens and their log probability, at this token position. In rare cases, there may be fewer than the number of requested `top_logprobs` returned.
|
||||
|
||||
token: string
|
||||
|
||||
The token.
|
||||
|
||||
bytes: array of number
|
||||
|
||||
A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token.
|
||||
|
||||
logprob: number
|
||||
|
||||
The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value `-9999.0` is used to signify that the token is very unlikely.
|
||||
|
||||
A list of message refusal tokens with log probability information.
|
||||
|
||||
token: string
|
||||
|
||||
The token.
|
||||
|
||||
bytes: array of number
|
||||
|
||||
A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token.
|
||||
|
||||
logprob: number
|
||||
|
||||
The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value `-9999.0` is used to signify that the token is very unlikely.
|
||||
|
||||
top\_logprobs: array of object { token, bytes, logprob }
|
||||
|
||||
List of the most likely tokens and their log probability, at this token position. In rare cases, there may be fewer than the number of requested `top_logprobs` returned.
|
||||
|
||||
token: string
|
||||
|
||||
The token.
|
||||
|
||||
bytes: array of number
|
||||
|
||||
A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token.
|
||||
|
||||
logprob: number
|
||||
|
||||
The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value `-9999.0` is used to signify that the token is very unlikely.
|
||||
|
||||
created: number
|
||||
|
||||
The Unix timestamp (in seconds) of when the chat completion was created. Each chunk has the same timestamp.
|
||||
|
||||
model: string
|
||||
|
||||
The model to generate the completion.
|
||||
|
||||
object: "chat.completion.chunk"
|
||||
|
||||
The object type, which is always `chat.completion.chunk`.
|
||||
|
||||
service\_tier: optional "auto" or "default" or "flex" or 2 more
|
||||
|
||||
Specifies the processing type used for serving the request.
|
||||
|
||||
- If set to 'auto', then the request will be processed with the service tier configured in the Project settings. Unless otherwise configured, the Project will use 'default'.
|
||||
- If set to 'default', then the request will be processed with the standard pricing and performance for the selected model.
|
||||
- If set to '[flex](https://developers.openai.com/docs/guides/flex-processing)' or '[priority](https://openai.com/api-priority-processing/)', then the request will be processed with the corresponding service tier.
|
||||
- When not set, the default behavior is 'auto'.
|
||||
|
||||
When the `service_tier` parameter is set, the response body will include the `service_tier` value based on the processing mode actually used to serve the request. This response value may be different from the value set in the parameter.
|
||||
|
||||
One of the following:
|
||||
|
||||
"auto"
|
||||
|
||||
"default"
|
||||
|
||||
"flex"
|
||||
|
||||
"scale"
|
||||
|
||||
"priority"
|
||||
|
||||
Deprecatedsystem\_fingerprint: optional string
|
||||
|
||||
This fingerprint represents the backend configuration that the model runs with. Can be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism.
|
||||
|
||||
usage: optional [CompletionUsage](https://developers.openai.com/api/reference/resources/completions#\(resource\)%20completions%20%3E%20\(model\)%20completion_usage%20%3E%20\(schema\)) { completion\_tokens, prompt\_tokens, total\_tokens, 2 more }
|
||||
|
||||
An optional field that will only be present when you set `stream_options: {"include_usage": true}` in your request. When present, it contains a null value **except for the last chunk** which contains the token usage statistics for the entire request.
|
||||
|
||||
**NOTE:** If the stream is interrupted or cancelled, you may not receive the final usage chunk which contains the total token usage for the request.
|
||||
|
||||
completion\_tokens: number
|
||||
|
||||
Number of tokens in the generated completion.
|
||||
|
||||
prompt\_tokens: number
|
||||
|
||||
Number of tokens in the prompt.
|
||||
|
||||
total\_tokens: number
|
||||
|
||||
Total number of tokens used in the request (prompt + completion).
|
||||
|
||||
completion\_tokens\_details: optional object { accepted\_prediction\_tokens, audio\_tokens, reasoning\_tokens, rejected\_prediction\_tokens }
|
||||
|
||||
Breakdown of tokens used in a completion.
|
||||
|
||||
accepted\_prediction\_tokens: optional number
|
||||
|
||||
When using Predicted Outputs, the number of tokens in the prediction that appeared in the completion.
|
||||
|
||||
audio\_tokens: optional number
|
||||
|
||||
Audio input tokens generated by the model.
|
||||
|
||||
reasoning\_tokens: optional number
|
||||
|
||||
Tokens generated by the model for reasoning.
|
||||
|
||||
rejected\_prediction\_tokens: optional number
|
||||
|
||||
When using Predicted Outputs, the number of tokens in the prediction that did not appear in the completion. However, like reasoning tokens, these tokens are still counted in the total completion tokens for purposes of billing, output, and context window limits.
|
||||
|
||||
prompt\_tokens\_details: optional object { audio\_tokens, cached\_tokens }
|
||||
|
||||
Breakdown of tokens used in the prompt.
|
||||
|
||||
audio\_tokens: optional number
|
||||
|
||||
Audio input tokens present in the prompt.
|
||||
|
||||
cached\_tokens: optional number
|
||||
|
||||
Cached tokens present in the prompt.
|
||||
799
docs/conversion_anthropic.md
Normal file
799
docs/conversion_anthropic.md
Normal file
@@ -0,0 +1,799 @@
|
||||
# Anthropic 协议适配清单
|
||||
|
||||
> 依据 [conversion_design.md](./conversion_design.md) 附录 D 模板编撰,覆盖 Anthropic API 的全部对接细节。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [协议基本信息](#1-协议基本信息)
|
||||
2. [接口识别](#2-接口识别)
|
||||
3. [请求头构建](#3-请求头构建)
|
||||
4. [核心层 — Chat 请求编解码](#4-核心层--chat-请求编解码)
|
||||
5. [核心层 — Chat 响应编解码](#5-核心层--chat-响应编解码)
|
||||
6. [核心层 — 流式编解码](#6-核心层--流式编解码)
|
||||
7. [扩展层接口](#7-扩展层接口)
|
||||
8. [错误编码](#8-错误编码)
|
||||
9. [自检清单](#9-自检清单)
|
||||
|
||||
---
|
||||
|
||||
## 1. 协议基本信息
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| 协议名称 | `"anthropic"` |
|
||||
| 协议版本 | `2023-06-01`(通过 `anthropic-version` Header 传递) |
|
||||
| Base URL | `https://api.anthropic.com` |
|
||||
| 认证方式 | `x-api-key: <api_key>` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 接口识别
|
||||
|
||||
### 2.1 URL 路径模式
|
||||
|
||||
| URL 路径 | InterfaceType |
|
||||
|----------|---------------|
|
||||
| `/v1/messages` | CHAT |
|
||||
| `/v1/models` | MODELS |
|
||||
| `/v1/models/{model}` | MODEL_INFO |
|
||||
| `/v1/batches` | 透传 |
|
||||
| `/v1/messages/count_tokens` | 透传 |
|
||||
| `/v1/*` | 透传 |
|
||||
|
||||
### 2.2 detectInterfaceType
|
||||
|
||||
```
|
||||
Anthropic.detectInterfaceType(nativePath):
|
||||
if nativePath == "/v1/messages": return CHAT
|
||||
if nativePath == "/v1/models": return MODELS
|
||||
if nativePath matches "^/v1/models/[^/]+$": return MODEL_INFO
|
||||
return PASSTHROUGH
|
||||
```
|
||||
|
||||
**说明**:`detectInterfaceType` 由 Anthropic Adapter 实现,根据 Anthropic 协议的 URL 路径约定识别接口类型。
|
||||
|
||||
### 2.3 接口能力矩阵
|
||||
|
||||
```
|
||||
Anthropic.supportsInterface(type):
|
||||
CHAT: return true
|
||||
MODELS: return true
|
||||
MODEL_INFO: return true
|
||||
EMBEDDINGS: return false // Anthropic 无此接口
|
||||
RERANK: return false // Anthropic 无此接口
|
||||
default: return false
|
||||
```
|
||||
|
||||
### 2.4 URL 映射表
|
||||
|
||||
```
|
||||
Anthropic.buildUrl(nativePath, interfaceType):
|
||||
switch interfaceType:
|
||||
case CHAT: return "/v1/messages"
|
||||
case MODELS: return "/v1/models"
|
||||
case MODEL_INFO: return "/v1/models/{modelId}"
|
||||
default: return nativePath
|
||||
```
|
||||
|
||||
不支持 EMBEDDINGS 和 RERANK(`supportsInterface` 返回 false),引擎会自动走透传逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 3. 请求头构建
|
||||
|
||||
### 3.1 buildHeaders
|
||||
|
||||
```
|
||||
Anthropic.buildHeaders(provider):
|
||||
result = {}
|
||||
result["x-api-key"] = provider.api_key
|
||||
result["anthropic-version"] = provider.adapter_config["anthropic_version"] ?? "2023-06-01"
|
||||
if provider.adapter_config["anthropic_beta"]:
|
||||
result["anthropic-beta"] = provider.adapter_config["anthropic_beta"].join(",")
|
||||
result["Content-Type"] = "application/json"
|
||||
return result
|
||||
```
|
||||
|
||||
### 3.2 adapter_config 契约
|
||||
|
||||
| Key | 类型 | 必填 | 默认值 | 说明 |
|
||||
|-----|------|------|--------|------|
|
||||
| `anthropic_version` | String | 否 | `"2023-06-01"` | API 版本,映射为 `anthropic-version` Header |
|
||||
| `anthropic_beta` | Array\<String\> | 否 | `[]` | Beta 功能标识列表,逗号拼接为 `anthropic-beta` Header |
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心层 — Chat 请求编解码
|
||||
|
||||
### 4.1 Decoder(Anthropic → Canonical)
|
||||
|
||||
#### 系统消息
|
||||
|
||||
```
|
||||
decodeSystem(system):
|
||||
if system is None: return None
|
||||
if system is String: return system
|
||||
return system.map(s => SystemBlock {text: s.text})
|
||||
```
|
||||
|
||||
Anthropic 使用顶层 `system` 字段(String 或 SystemBlock 数组),直接提取为 `canonical.system`。
|
||||
|
||||
#### 消息角色映射
|
||||
|
||||
| Anthropic role | Canonical role | 说明 |
|
||||
|----------------|---------------|------|
|
||||
| `user` | `user` | 直接映射;可能包含 tool_result |
|
||||
| `assistant` | `assistant` | 直接映射 |
|
||||
|
||||
**关键差异**:Anthropic 没有 `system` 和 `tool` 角色。system 通过顶层字段传递;tool_result 嵌入 user 消息的 content 数组中。
|
||||
|
||||
#### 内容块解码
|
||||
|
||||
```
|
||||
decodeContentBlocks(content):
|
||||
if content is String: return [{type: "text", text: content}]
|
||||
return content.map(block => {
|
||||
switch block.type:
|
||||
"text" → TextBlock{text: block.text}
|
||||
"tool_use" → ToolUseBlock{id: block.id, name: block.name, input: block.input}
|
||||
"tool_result" → ToolResultBlock{tool_use_id: block.tool_use_id, ...}
|
||||
"thinking" → ThinkingBlock{thinking: block.thinking}
|
||||
"redacted_thinking" → 丢弃 // 仅 Anthropic 使用,不在中间层保留
|
||||
})
|
||||
```
|
||||
|
||||
**tool_result 角色转换**:
|
||||
|
||||
```
|
||||
decodeMessage(msg):
|
||||
switch msg.role:
|
||||
case "user":
|
||||
blocks = decodeContentBlocks(msg.content)
|
||||
toolResults = blocks.filter(b => b.type == "tool_result")
|
||||
others = blocks.filter(b => b.type != "tool_result")
|
||||
if toolResults.length > 0:
|
||||
return [
|
||||
...(others.length > 0 ? [{role: "user", content: others}] : []),
|
||||
{role: "tool", content: toolResults}]
|
||||
return [{role: "user", content: blocks}]
|
||||
case "assistant":
|
||||
return [{role: "assistant", content: decodeContentBlocks(msg.content)}]
|
||||
```
|
||||
|
||||
Anthropic user 消息中的 `tool_result` 块被拆分为独立的 Canonical `tool` 角色消息。
|
||||
|
||||
#### 工具定义
|
||||
|
||||
| Anthropic | Canonical | 说明 |
|
||||
|-----------|-----------|------|
|
||||
| `tools[].name` | `tools[].name` | 直接映射 |
|
||||
| `tools[].description` | `tools[].description` | 直接映射 |
|
||||
| `tools[].input_schema` | `tools[].input_schema` | 字段名相同 |
|
||||
| `tools[].type` | — | Anthropic 无 function 包装层 |
|
||||
|
||||
#### 工具选择
|
||||
|
||||
| Anthropic tool_choice | Canonical ToolChoice |
|
||||
|-----------------------|---------------------|
|
||||
| `{type: "auto"}` | `{type: "auto"}` |
|
||||
| `{type: "none"}` | `{type: "none"}` |
|
||||
| `{type: "any"}` | `{type: "any"}` |
|
||||
| `{type: "tool", name}` | `{type: "tool", name}` |
|
||||
|
||||
#### 参数映射
|
||||
|
||||
| Anthropic | Canonical | 说明 |
|
||||
|-----------|-----------|------|
|
||||
| `max_tokens` | `parameters.max_tokens` | 直接映射;Anthropic 必填 |
|
||||
| `temperature` | `parameters.temperature` | 直接映射 |
|
||||
| `top_p` | `parameters.top_p` | 直接映射 |
|
||||
| `top_k` | `parameters.top_k` | 直接映射 |
|
||||
| `stop_sequences` (Array) | `parameters.stop_sequences` (Array) | 直接映射 |
|
||||
| `stream` | `stream` | 直接映射 |
|
||||
|
||||
#### 新增公共字段
|
||||
|
||||
```
|
||||
decodeExtras(raw):
|
||||
user_id = raw.metadata?.user_id
|
||||
output_format = decodeOutputFormat(raw.output_config)
|
||||
parallel_tool_use = raw.disable_parallel_tool_use == true ? false : null
|
||||
thinking = raw.thinking ? ThinkingConfig {
|
||||
type: raw.thinking.type, // "enabled" | "disabled" | "adaptive"
|
||||
budget_tokens: raw.thinking.budget_tokens,
|
||||
effort: raw.output_config?.effort } : null
|
||||
```
|
||||
|
||||
**ThinkingConfig 三种类型解码**:
|
||||
|
||||
| Anthropic thinking.type | Canonical thinking.type | 说明 |
|
||||
|-------------------------|----------------------|------|
|
||||
| `"enabled"` | `"enabled"` | 有 budget_tokens,直接映射 |
|
||||
| `"disabled"` | `"disabled"` | 直接映射 |
|
||||
| `"adaptive"` | `"adaptive"` | Anthropic 自动决定是否启用思考,映射为 `"adaptive"`(新增 Canonical 值) |
|
||||
|
||||
> **注意**:`thinking.display`(`"summarized"` / `"omitted"`)为 Anthropic 特有字段,控制响应中思考内容的显示方式,不晋升为公共字段。
|
||||
|
||||
**output_config 解码**:
|
||||
|
||||
```
|
||||
decodeOutputFormat(output_config):
|
||||
if output_config?.format?.type == "json_schema":
|
||||
return { type: "json_schema", json_schema: { name: "output", schema: output_config.format.schema, strict: true } }
|
||||
return null
|
||||
```
|
||||
|
||||
| Anthropic | Canonical | 提取规则 |
|
||||
|-----------|-----------|---------|
|
||||
| `metadata.user_id` | `user_id` | 从嵌套对象提取 |
|
||||
| `output_config.format` | `output_format` | 仅支持 `json_schema` 类型;映射为 Canonical OutputFormat |
|
||||
| `output_config.effort` | `thinking.effort` | `"low"` / `"medium"` / `"high"` / `"xhigh"` / `"max"` 直接映射 |
|
||||
| `disable_parallel_tool_use` | `parallel_tool_use` | **语义反转**:true → false |
|
||||
| `thinking.type` | `thinking.type` | 直接映射 |
|
||||
| `thinking.budget_tokens` | `thinking.budget_tokens` | 直接映射 |
|
||||
|
||||
#### 协议特有字段
|
||||
|
||||
| 字段 | 处理方式 |
|
||||
|------|---------|
|
||||
| `cache_control` | 忽略(仅 Anthropic 使用,不晋升为公共字段) |
|
||||
| `redacted_thinking` | 解码时丢弃,不在中间层保留 |
|
||||
| `metadata` (除 user_id) | 忽略 |
|
||||
| `thinking.display` | 忽略(控制响应显示方式,不影响请求语义) |
|
||||
| `container` | 忽略(容器标识,协议特有) |
|
||||
| `inference_geo` | 忽略(地理区域控制,协议特有) |
|
||||
| `service_tier` | 忽略(服务层级选择,协议特有) |
|
||||
|
||||
#### 协议约束
|
||||
|
||||
- `max_tokens` 为**必填**字段
|
||||
- messages 必须以 `user` 角色开始
|
||||
- `user` 和 `assistant` 角色必须严格交替(除连续 tool_result 场景)
|
||||
- tool_result 必须紧跟在包含对应 tool_use 的 assistant 消息之后
|
||||
|
||||
### 4.2 Encoder(Canonical → Anthropic)
|
||||
|
||||
#### 模型名称
|
||||
|
||||
使用 `provider.model_name` 覆盖 `canonical.model`。
|
||||
|
||||
#### 系统消息注入
|
||||
|
||||
```
|
||||
encodeSystem(system):
|
||||
if system is String: return system
|
||||
return system.map(s => ({text: s.text}))
|
||||
```
|
||||
|
||||
将 `canonical.system` 编码为 Anthropic 顶层 `system` 字段。
|
||||
|
||||
#### 消息编码
|
||||
|
||||
**关键差异**:Canonical 的 `tool` 角色消息需要合并到 Anthropic 的 `user` 消息中:
|
||||
|
||||
```
|
||||
encodeMessages(canonical):
|
||||
result = []
|
||||
for msg in canonical.messages:
|
||||
switch msg.role:
|
||||
case "user":
|
||||
result.append({role: "user", content: encodeContentBlocks(msg.content)})
|
||||
case "assistant":
|
||||
result.append({role: "assistant", content: encodeContentBlocks(msg.content)})
|
||||
case "tool":
|
||||
// tool 角色转为 Anthropic 的 user 消息内 tool_result 块
|
||||
toolResults = msg.content.filter(b => b.type == "tool_result")
|
||||
if result.length > 0 && result.last.role == "user":
|
||||
result.last.content = result.last.content + toolResults
|
||||
else:
|
||||
result.append({role: "user", content: toolResults})
|
||||
```
|
||||
|
||||
#### 角色约束处理
|
||||
|
||||
Anthropic 要求 user/assistant 严格交替。编码时需要:
|
||||
1. 将 Canonical `tool` 角色合并到相邻 `user` 消息中
|
||||
2. 确保首条消息为 `user` 角色(若无,自动注入空 user 消息)
|
||||
3. 合并连续同角色消息
|
||||
|
||||
#### 工具编码
|
||||
|
||||
```
|
||||
encodeTools(canonical):
|
||||
if canonical.tools:
|
||||
result.tools = canonical.tools.map(t => ({
|
||||
name: t.name, description: t.description, input_schema: t.input_schema}))
|
||||
|
||||
encodeToolChoice(choice):
|
||||
switch choice.type:
|
||||
"auto" → {type: "auto"}
|
||||
"none" → {type: "none"}
|
||||
"any" → {type: "any"}
|
||||
"tool" → {type: "tool", name: choice.name}
|
||||
```
|
||||
|
||||
#### 公共字段编码
|
||||
|
||||
```
|
||||
encodeRequest(canonical, provider):
|
||||
result = {
|
||||
model: provider.model_name,
|
||||
messages: encodeMessages(canonical),
|
||||
max_tokens: canonical.parameters.max_tokens,
|
||||
temperature: canonical.parameters.temperature,
|
||||
top_p: canonical.parameters.top_p,
|
||||
top_k: canonical.parameters.top_k,
|
||||
stream: canonical.stream
|
||||
}
|
||||
if canonical.system:
|
||||
result.system = encodeSystem(canonical.system)
|
||||
if canonical.parameters.stop_sequences:
|
||||
result.stop_sequences = canonical.parameters.stop_sequences
|
||||
if canonical.user_id:
|
||||
result.metadata = {user_id: canonical.user_id}
|
||||
if canonical.output_format or canonical.thinking?.effort:
|
||||
result.output_config = {}
|
||||
if canonical.output_format:
|
||||
result.output_config.format = encodeOutputFormat(canonical.output_format)
|
||||
if canonical.thinking?.effort:
|
||||
result.output_config.effort = canonical.thinking.effort
|
||||
if canonical.parallel_tool_use == false:
|
||||
result.disable_parallel_tool_use = true
|
||||
if canonical.tools:
|
||||
result.tools = canonical.tools.map(t => ({
|
||||
name: t.name, description: t.description, input_schema: t.input_schema}))
|
||||
if canonical.tool_choice:
|
||||
result.tool_choice = encodeToolChoice(canonical.tool_choice)
|
||||
if canonical.thinking:
|
||||
result.thinking = encodeThinkingConfig(canonical.thinking)
|
||||
return result
|
||||
|
||||
encodeThinkingConfig(canonical):
|
||||
switch canonical.type:
|
||||
"enabled":
|
||||
cfg = {type: "enabled", budget_tokens: canonical.budget_tokens}
|
||||
return cfg
|
||||
"disabled":
|
||||
return {type: "disabled"}
|
||||
"adaptive":
|
||||
return {type: "adaptive"}
|
||||
return {type: "disabled"}
|
||||
|
||||
encodeOutputFormat(output_format):
|
||||
switch output_format.type:
|
||||
"json_schema":
|
||||
return {type: "json_schema", schema: output_format.json_schema.schema}
|
||||
"json_object":
|
||||
return {type: "json_schema", schema: {type: "object"}}
|
||||
```
|
||||
|
||||
#### 降级处理
|
||||
|
||||
对照架构文档 §8.4 三级降级策略,确认每个不支持字段的处理:
|
||||
|
||||
| Canonical 字段 | Anthropic 不支持时 | 降级策略 |
|
||||
|---------------|-------------------|---------|
|
||||
| `thinking.effort` | Anthropic 通过 `output_config.effort` 传递 | 自动映射为 `output_config.effort` |
|
||||
| `stop_reason: "content_filter"` | Anthropic 无此值 | 自动映射为 `"end_turn"` |
|
||||
| `output_format: "text"` | Anthropic 无 text 输出格式 | 丢弃,不设置 output_config |
|
||||
| `output_format: "json_object"` | Anthropic 用 json_schema 替代 | 替代方案:生成空 schema 的 json_schema |
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心层 — Chat 响应编解码
|
||||
|
||||
逐字段对照 §4.7 CanonicalResponse 确认映射关系。
|
||||
|
||||
### 5.1 响应结构
|
||||
|
||||
```
|
||||
Anthropic 响应顶层结构:
|
||||
{
|
||||
id: String,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
model: String,
|
||||
content: [ContentBlock...],
|
||||
stop_reason: String,
|
||||
stop_sequence: String | null,
|
||||
stop_details: Object | null,
|
||||
container: Object | null,
|
||||
usage: { input_tokens, output_tokens, cache_read_input_tokens?, cache_creation_input_tokens?,
|
||||
cache_creation?, inference_geo?, server_tool_use?, service_tier? }
|
||||
}
|
||||
```
|
||||
|
||||
**新增字段**(对比 §4.7 CanonicalResponse):
|
||||
|
||||
| Anthropic 字段 | 说明 |
|
||||
|----------------|------|
|
||||
| `stop_details` | 结构化拒绝信息:`{type: "refusal", category, explanation}`,仅 `stop_reason == "refusal"` 时存在 |
|
||||
| `container` | 容器信息:`{id, expires_at}`,仅使用 code execution 工具时存在 |
|
||||
|
||||
### 5.2 Decoder(Anthropic → Canonical)
|
||||
|
||||
```
|
||||
decodeResponse(anthropicResp):
|
||||
blocks = []
|
||||
for block in anthropicResp.content:
|
||||
switch block.type:
|
||||
"text" → blocks.append({type: "text", text: block.text})
|
||||
"tool_use" → blocks.append({type: "tool_use", id: block.id, name: block.name, input: block.input})
|
||||
"thinking" → blocks.append({type: "thinking", thinking: block.thinking})
|
||||
"redacted_thinking" → 丢弃 // 仅 Anthropic 使用,不在中间层保留
|
||||
return CanonicalResponse {id, model, content: blocks, stop_reason: mapStopReason(anthropicResp.stop_reason),
|
||||
usage: CanonicalUsage {input_tokens, output_tokens,
|
||||
cache_read_tokens: anthropicResp.usage.cache_read_input_tokens,
|
||||
cache_creation_tokens: anthropicResp.usage.cache_creation_input_tokens}}
|
||||
```
|
||||
|
||||
**内容块解码**:
|
||||
- `text` → TextBlock(直接映射;忽略 `citations` 字段)
|
||||
- `tool_use` → ToolUseBlock(直接映射;忽略 `caller` 字段)
|
||||
- `thinking` → ThinkingBlock(直接映射;忽略 `signature` 字段)
|
||||
- `redacted_thinking` → 丢弃(协议特有,不晋升为公共字段)
|
||||
- `server_tool_use` / `web_search_tool_result` / `code_execution_tool_result` 等 → 丢弃(服务端工具块,协议特有)
|
||||
|
||||
**停止原因映射**:
|
||||
|
||||
| Anthropic stop_reason | Canonical stop_reason | 说明 |
|
||||
|-----------------------|-----------------------|------|
|
||||
| `"end_turn"` | `"end_turn"` | 直接映射 |
|
||||
| `"max_tokens"` | `"max_tokens"` | 直接映射 |
|
||||
| `"tool_use"` | `"tool_use"` | 直接映射 |
|
||||
| `"stop_sequence"` | `"stop_sequence"` | 直接映射 |
|
||||
| `"pause_turn"` | `"pause_turn"` | 长轮次暂停,映射为 Canonical 新增值 |
|
||||
| `"refusal"` | `"refusal"` | 安全拒绝,直接映射 |
|
||||
|
||||
**Token 用量映射**:
|
||||
|
||||
| Anthropic usage | Canonical Usage | 说明 |
|
||||
|-----------------|-----------------|------|
|
||||
| `input_tokens` | `input_tokens` | 直接映射 |
|
||||
| `output_tokens` | `output_tokens` | 直接映射 |
|
||||
| `cache_read_input_tokens` | `cache_read_tokens` | 字段名映射 |
|
||||
| `cache_creation_input_tokens` | `cache_creation_tokens` | 字段名映射 |
|
||||
| `cache_creation` | — | 协议特有(按 TTL 细分),不晋升 |
|
||||
| `inference_geo` | — | 协议特有,不晋升 |
|
||||
| `server_tool_use` | — | 协议特有,不晋升 |
|
||||
| `service_tier` | — | 协议特有,不晋升 |
|
||||
| — | `reasoning_tokens` | Anthropic 不返回此字段,始终为 null |
|
||||
|
||||
**协议特有内容**:
|
||||
|
||||
| 字段 | 处理方式 |
|
||||
|------|---------|
|
||||
| `redacted_thinking` | 解码时丢弃 |
|
||||
| `stop_sequence` | 解码时忽略(Canonical 用 stop_reason 覆盖) |
|
||||
| `stop_details` | 解码时忽略(协议特有,不晋升) |
|
||||
| `container` | 解码时忽略(协议特有,不晋升) |
|
||||
| `text.citations` | 解码时忽略(协议特有,不晋升) |
|
||||
| `tool_use.caller` | 解码时忽略(协议特有,不晋升) |
|
||||
| `thinking.signature` | 解码时忽略(协议特有,不晋升;同协议透传时自然保留) |
|
||||
|
||||
### 5.3 Encoder(Canonical → Anthropic)
|
||||
|
||||
```
|
||||
encodeResponse(canonical):
|
||||
blocks = canonical.content.map(block => {
|
||||
switch block.type:
|
||||
"text" → {type: "text", text: block.text}
|
||||
"tool_use" → {type: "tool_use", id: block.id, name: block.name, input: block.input}
|
||||
"thinking" → {type: "thinking", thinking: block.thinking}})
|
||||
return {id: canonical.id, type: "message", role: "assistant", model: canonical.model,
|
||||
content: blocks,
|
||||
stop_reason: mapCanonicalStopReason(canonical.stop_reason),
|
||||
stop_sequence: None,
|
||||
usage: {input_tokens: canonical.usage.input_tokens, output_tokens: canonical.usage.output_tokens,
|
||||
cache_read_input_tokens: canonical.usage.cache_read_tokens,
|
||||
cache_creation_input_tokens: canonical.usage.cache_creation_tokens}}
|
||||
```
|
||||
|
||||
**内容块编码**:
|
||||
- TextBlock → `{type: "text", text}`(直接映射)
|
||||
- ToolUseBlock → `{type: "tool_use", id, name, input}`(直接映射)
|
||||
- ThinkingBlock → `{type: "thinking", thinking}`(直接映射)
|
||||
|
||||
**停止原因映射**:
|
||||
|
||||
| Canonical stop_reason | Anthropic stop_reason |
|
||||
|-----------------------|-----------------------|
|
||||
| `"end_turn"` | `"end_turn"` |
|
||||
| `"max_tokens"` | `"max_tokens"` |
|
||||
| `"tool_use"` | `"tool_use"` |
|
||||
| `"stop_sequence"` | `"stop_sequence"` |
|
||||
| `"pause_turn"` | `"pause_turn"` |
|
||||
| `"refusal"` | `"refusal"` |
|
||||
| `"content_filter"` | `"end_turn"`(降级) |
|
||||
|
||||
**降级处理**:
|
||||
|
||||
| Canonical 字段 | Anthropic 不支持时 | 降级策略 |
|
||||
|---------------|-------------------|---------|
|
||||
| `stop_reason: "content_filter"` | Anthropic 无此值 | 自动映射为 `"end_turn"` |
|
||||
| `reasoning_tokens` | Anthropic 无此字段 | 丢弃 |
|
||||
|
||||
**协议特有内容**:
|
||||
|
||||
| 字段 | 处理方式 |
|
||||
|------|---------|
|
||||
| `redacted_thinking` | 编码时不产出 |
|
||||
| `stop_sequence` | 编码时始终为 null |
|
||||
| `stop_details` | 编码时不产出 |
|
||||
| `container` | 编码时不产出 |
|
||||
| `text.citations` | 编码时不产出 |
|
||||
| `thinking.signature` | 编码时不产出(同协议透传时自然保留) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 核心层 — 流式编解码
|
||||
|
||||
### 6.1 SSE 格式
|
||||
|
||||
Anthropic 使用命名 SSE 事件,与 CanonicalStreamEvent 几乎 1:1 对应:
|
||||
|
||||
```
|
||||
event: message_start
|
||||
data: {"type":"message_start","message":{"id":"msg_xxx","model":"claude-4",...}}
|
||||
|
||||
event: content_block_start
|
||||
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
|
||||
|
||||
event: content_block_delta
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
|
||||
|
||||
event: content_block_stop
|
||||
data: {"type":"content_block_stop","index":0}
|
||||
|
||||
event: message_delta
|
||||
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":10}}
|
||||
|
||||
event: message_stop
|
||||
data: {"type":"message_stop"}
|
||||
|
||||
event: ping
|
||||
data: {"type":"ping"}
|
||||
```
|
||||
|
||||
### 6.2 StreamDecoder(Anthropic SSE → Canonical 事件)
|
||||
|
||||
Anthropic SSE 事件与 CanonicalStreamEvent 几乎 1:1 映射,状态机最简单:
|
||||
|
||||
| Anthropic SSE 事件 | Canonical 事件 | 说明 |
|
||||
|---|---|---|
|
||||
| `message_start` | MessageStartEvent | 直接映射 |
|
||||
| `content_block_start` | ContentBlockStartEvent | 直接映射 content_block |
|
||||
| `content_block_delta` | ContentBlockDeltaEvent | 见下方 delta 类型映射表 |
|
||||
| `content_block_stop` | ContentBlockStopEvent | 直接映射 |
|
||||
| `message_delta` | MessageDeltaEvent | 直接映射 delta 和 usage |
|
||||
| `message_stop` | MessageStopEvent | 直接映射 |
|
||||
| `ping` | PingEvent | 直接映射 |
|
||||
| `error` | ErrorEvent | 直接映射 |
|
||||
|
||||
**delta 类型映射**(`content_block_delta` 事件内):
|
||||
|
||||
| Anthropic delta 类型 | Canonical delta 类型 | 说明 |
|
||||
|---------------------|---------------------|------|
|
||||
| `text_delta` | `{type: "text_delta", text}` | 直接映射 |
|
||||
| `input_json_delta` | `{type: "input_json_delta", partial_json}` | 直接映射 |
|
||||
| `thinking_delta` | `{type: "thinking_delta", thinking}` | 直接映射 |
|
||||
| `citations_delta` | 丢弃 | 协议特有,不晋升为公共字段 |
|
||||
| `signature_delta` | 丢弃 | 协议特有(用于多轮思考签名连续性),不晋升 |
|
||||
|
||||
**content_block_start 类型映射**:
|
||||
|
||||
| Anthropic content_block 类型 | Canonical content_block | 说明 |
|
||||
|------------------------------|----------------------|------|
|
||||
| `{type: "text", text: ""}` | `{type: "text", text: ""}` | 直接映射 |
|
||||
| `{type: "tool_use", id, name, input: {}}` | `{type: "tool_use", id, name, input: {}}` | 直接映射 |
|
||||
| `{type: "thinking", thinking: ""}` | `{type: "thinking", thinking: ""}` | 直接映射 |
|
||||
| `{type: "redacted_thinking", data: ""}` | 丢弃整个 block | 跳过后续 delta 直到 content_block_stop |
|
||||
| `server_tool_use` / `web_search_tool_result` 等 | 丢弃 | 服务端工具块,协议特有 |
|
||||
|
||||
### 6.3 StreamDecoder 状态机
|
||||
|
||||
```
|
||||
StreamDecoderState {
|
||||
messageStarted: Boolean
|
||||
openBlocks: Set<Integer>
|
||||
currentBlockType: Map<Integer, String>
|
||||
currentBlockId: Map<Integer, String>
|
||||
redactedBlocks: Set<Integer> // 追踪需要丢弃的 redacted_thinking block
|
||||
utf8Remainder: Option<ByteArray> // UTF-8 跨 chunk 安全
|
||||
accumulatedUsage: Option<CanonicalUsage>
|
||||
}
|
||||
```
|
||||
|
||||
Anthropic Decoder 无需 OpenAI 的 `toolCallIdMap` / `toolCallNameMap` / `toolCallArguments`,因为 Anthropic 的事件已经有明确的结构。
|
||||
|
||||
**关键处理**:
|
||||
|
||||
- **`redacted_thinking`**:在 `content_block_start` 事件中检测类型,将 index 加入 `redactedBlocks`,后续 delta 和 stop 事件均丢弃
|
||||
- **`citations_delta` / `signature_delta`**:在 delta 映射时直接丢弃,不影响 block 生命周期
|
||||
- **`server_tool_use` 等服务端工具块**:与 `redacted_thinking` 处理方式一致,加入 `redactedBlocks` 丢弃
|
||||
- **UTF-8 安全**:跨 chunk 截断的 UTF-8 字节需要用 `utf8Remainder` 缓冲
|
||||
- **usage 累积**:`message_delta` 中的 usage 与 `message_start` 中的 usage 合并
|
||||
|
||||
### 6.4 StreamEncoder(Canonical → Anthropic SSE)
|
||||
|
||||
| Canonical 事件 | Anthropic SSE 事件 | 说明 |
|
||||
|---|---|---|
|
||||
| MessageStartEvent | `event: message_start` | 直接映射 |
|
||||
| ContentBlockStartEvent | `event: content_block_start` | 直接映射 content_block |
|
||||
| ContentBlockDeltaEvent | `event: content_block_delta` | 见下方 delta 编码表 |
|
||||
| ContentBlockStopEvent | `event: content_block_stop` | 直接映射 |
|
||||
| MessageDeltaEvent | `event: message_delta` | 直接映射 |
|
||||
| MessageStopEvent | `event: message_stop` | 直接映射 |
|
||||
| PingEvent | `event: ping` | 直接映射 |
|
||||
| ErrorEvent | `event: error` | 直接映射 |
|
||||
|
||||
**delta 编码表**:
|
||||
|
||||
| Canonical delta 类型 | Anthropic delta 类型 | 说明 |
|
||||
|---------------------|---------------------|------|
|
||||
| `{type: "text_delta", text}` | `text_delta` | 直接映射 |
|
||||
| `{type: "input_json_delta", partial_json}` | `input_json_delta` | 直接映射 |
|
||||
| `{type: "thinking_delta", thinking}` | `thinking_delta` | 直接映射 |
|
||||
|
||||
**缓冲策略**:无需缓冲,每个 Canonical 事件直接编码为对应的 Anthropic SSE 事件。
|
||||
|
||||
**SSE 编码格式**:
|
||||
|
||||
```
|
||||
event: <event_type>\n
|
||||
data: <json_payload>\n
|
||||
\n
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 扩展层接口
|
||||
|
||||
### 7.1 /models & /models/{model}
|
||||
|
||||
**列表接口** `GET /v1/models`:
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| 接口是否存在 | 是 |
|
||||
| 请求格式 | GET 请求,支持 `limit`、`after_id`、`before_id` 查询参数 |
|
||||
|
||||
响应 Decoder(Anthropic → Canonical):
|
||||
|
||||
```
|
||||
decodeModelsResponse(anthropicResp):
|
||||
return CanonicalModelList {
|
||||
models: anthropicResp.data.map(m => CanonicalModel {
|
||||
id: m.id, name: m.display_name ?? m.id, created: parseTimestamp(m.created_at),
|
||||
owned_by: "anthropic"})}
|
||||
|
||||
parseTimestamp(timestamp):
|
||||
// Anthropic 返回 RFC 3339 字符串(如 "2025-05-14T00:00:00Z"),需转为 Unix 时间戳
|
||||
return rfc3339ToUnix(timestamp) ?? 0
|
||||
```
|
||||
|
||||
响应 Encoder(Canonical → Anthropic):
|
||||
|
||||
```
|
||||
encodeModelsResponse(canonical):
|
||||
return {data: canonical.models.map(m => ({
|
||||
id: m.id,
|
||||
display_name: m.name ?? m.id,
|
||||
created_at: m.created ? unixToRfc3339(m.created) : epochRfc3339(),
|
||||
type: "model"})),
|
||||
has_more: false,
|
||||
first_id: canonical.models[0]?.id, last_id: canonical.models.last?.id}
|
||||
```
|
||||
|
||||
**详情接口** `GET /v1/models/{model}`:
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| 接口是否存在 | 是 |
|
||||
| 请求格式 | GET 请求,路径参数 `model_id` |
|
||||
|
||||
响应 Decoder(Anthropic → Canonical):
|
||||
|
||||
```
|
||||
decodeModelInfoResponse(anthropicResp):
|
||||
return CanonicalModelInfo {
|
||||
id: anthropicResp.id, name: anthropicResp.display_name ?? anthropicResp.id,
|
||||
created: parseTimestamp(anthropicResp.created_at), owned_by: "anthropic" }
|
||||
```
|
||||
|
||||
响应 Encoder(Canonical → Anthropic):
|
||||
|
||||
```
|
||||
encodeModelInfoResponse(canonical):
|
||||
return {id: canonical.id,
|
||||
display_name: canonical.name ?? canonical.id,
|
||||
created_at: canonical.created ? unixToRfc3339(canonical.created) : epochRfc3339(),
|
||||
type: "model"}
|
||||
```
|
||||
|
||||
**字段映射**(列表和详情共用):
|
||||
|
||||
| Anthropic | Canonical | 说明 |
|
||||
|-----------|-----------|------|
|
||||
| `data[].id` | `models[].id` | 直接映射 |
|
||||
| `data[].display_name` | `models[].name` | Anthropic 特有的显示名称 |
|
||||
| `data[].created_at` | `models[].created` | **类型转换**:Anthropic 为 RFC 3339 字符串,Canonical 为 Unix 时间戳 |
|
||||
| `data[].type: "model"` | — | 固定值 |
|
||||
| `has_more` | — | 编码时固定为 false |
|
||||
| `first_id` / `last_id` | — | 从列表提取 |
|
||||
| `data[].capabilities` | — | 协议特有,不晋升 |
|
||||
| `data[].max_input_tokens` | — | 协议特有,不晋升 |
|
||||
| `data[].max_tokens` | — | 协议特有,不晋升 |
|
||||
|
||||
**跨协议对接示例**(入站 `/anthropic/v1/models`,目标 OpenAI):
|
||||
|
||||
```
|
||||
入站: GET /anthropic/v1/models, x-api-key: sk-ant-xxx
|
||||
→ client=anthropic, provider=openai
|
||||
→ URL: /v1/models, Headers: Authorization: Bearer sk-xxx
|
||||
|
||||
OpenAI 上游响应: {object: "list", data: [{id: "gpt-4o", object: "model", created: 1700000000, owned_by: "openai"}]}
|
||||
→ OpenAI.decodeModelsResponse → CanonicalModelList
|
||||
→ Anthropic.encodeModelsResponse
|
||||
|
||||
返回客户端: {data: [{id: "gpt-4o", display_name: "gpt-4o", created_at: "2023-11-04T18:26:40Z", type: "model"}],
|
||||
has_more: false, first_id: "gpt-4o", last_id: "gpt-4o"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 错误编码
|
||||
|
||||
### 8.1 错误响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "error",
|
||||
"error": {
|
||||
"type": "invalid_request_error",
|
||||
"message": "Error message"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 encodeError
|
||||
|
||||
```
|
||||
Anthropic.encodeError(error):
|
||||
return {type: "error", error: {type: error.code, message: error.message}}
|
||||
```
|
||||
|
||||
### 8.3 常用 HTTP 状态码
|
||||
|
||||
| HTTP Status | 说明 |
|
||||
|-------------|------|
|
||||
| 400 | 请求格式错误 |
|
||||
| 401 | 认证失败(无效 API key) |
|
||||
| 403 | 无权限访问 |
|
||||
| 404 | 接口不存在 |
|
||||
| 429 | 速率限制 |
|
||||
| 500 | 服务器内部错误 |
|
||||
| 529 | 服务过载 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 自检清单
|
||||
|
||||
| 章节 | 检查项 |
|
||||
|------|--------|
|
||||
| §2 | [x] `detectInterfaceType(nativePath)` 已实现,所有已知路径已覆盖 |
|
||||
| §2 | [x] 所有 InterfaceType 的 `supportsInterface` 返回值已确定 |
|
||||
| §2 | [x] 所有 InterfaceType 的 `buildUrl` 映射已确定 |
|
||||
| §3 | [x] `buildHeaders(provider)` 已实现,adapter_config 契约已文档化 |
|
||||
| §4 | [x] Chat 请求的 Decoder 和 Encoder 已实现(逐字段对照 §4.1/§4.2) |
|
||||
| §4 | [x] 角色映射和消息顺序约束已处理(tool→user 合并、首消息 user 保证、交替约束) |
|
||||
| §4 | [x] 工具调用(tool_use / tool_result)的编解码已处理 |
|
||||
| §4 | [x] 协议特有字段已识别并确定处理方式(cache_control 忽略、redacted_thinking 丢弃) |
|
||||
| §5 | [x] Chat 响应的 Decoder 和 Encoder 已实现(逐字段对照 §4.7) |
|
||||
| §5 | [x] stop_reason 映射表已确认 |
|
||||
| §5 | [x] usage 字段映射已确认(input_tokens / cache_read_input_tokens 等) |
|
||||
| §6 | [x] 流式 StreamDecoder 和 StreamEncoder 已实现(对照 §4.8) |
|
||||
| §7 | [x] 扩展层接口的编解码已实现(/models、/models/{model}) |
|
||||
| §8 | [x] `encodeError` 已实现 |
|
||||
File diff suppressed because it is too large
Load Diff
1180
docs/conversion_openai.md
Normal file
1180
docs/conversion_openai.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-19
|
||||
288
openspec/changes/refactor-conversion-engine/design.md
Normal file
288
openspec/changes/refactor-conversion-engine/design.md
Normal file
@@ -0,0 +1,288 @@
|
||||
## Context
|
||||
|
||||
### 现有架构
|
||||
|
||||
当前后端协议转换以 OpenAI 类型为内部枢纽,整体结构:
|
||||
|
||||
```
|
||||
Anthropic Handler ──▶ anthropic.ConvertRequest() ──▶ openai.ChatCompletionRequest
|
||||
│
|
||||
OpenAI Handler ──────────────────────────────────▶ openai.ChatCompletionRequest
|
||||
│
|
||||
▼
|
||||
ProviderClient
|
||||
(硬编码 OpenAI Adapter)
|
||||
│
|
||||
▼
|
||||
上游 OpenAI 兼容 API
|
||||
```
|
||||
|
||||
关键文件:
|
||||
- `internal/protocol/openai/types.go` — OpenAI 线路格式类型,兼作内部枢纽格式
|
||||
- `internal/protocol/anthropic/converter.go` — Anthropic→OpenAI 单向转换
|
||||
- `internal/protocol/anthropic/stream_converter.go` — OpenAI chunk→Anthropic SSE 单向流式转换
|
||||
- `internal/handler/openai_handler.go` — OpenAI 请求处理
|
||||
- `internal/handler/anthropic_handler.go` — Anthropic 请求处理,内含协议转换编排
|
||||
- `internal/provider/client.go` — HTTP 客户端,硬编码 `openai.Adapter` 做序列化/反序列化
|
||||
|
||||
### 核心限制
|
||||
|
||||
1. **单向转换**:只有 Anthropic→OpenAI,无反向能力
|
||||
2. **OpenAI 绑定**:上游通信只能走 OpenAI 协议
|
||||
3. **无透传**:即使 client==provider(同协议),仍走完整序列化/反序列化
|
||||
4. **无扩展性**:新增协议需修改多处代码,无统一接入点
|
||||
5. **仅 Chat**:只支持 `/v1/chat/completions` 和 `/v1/messages` 两个固定端点,无 Models/Embeddings/Rerank
|
||||
|
||||
### 设计参考
|
||||
|
||||
三份设计文档已完整定义目标架构和两个协议的适配细节:
|
||||
- `docs/conversion_design.md` — 整体架构(Hub-and-Spoke、Canonical Model、ProtocolAdapter 接口、ConversionEngine、流式管道、错误处理)
|
||||
- `docs/conversion_openai.md` — OpenAI 协议适配清单(字段映射、流式状态机、角色合并等)
|
||||
- `docs/conversion_anthropic.md` — Anthropic 协议适配清单(角色约束、thinking、流式命名事件等)
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 实现完整的 Hub-and-Spoke 协议转换架构,以 Canonical Model 为枢纽
|
||||
- 支持任意协议对的请求/响应双向转换(当前:OpenAI ↔ Anthropic)
|
||||
- 支持同协议透传(零语义损失、零序列化开销)
|
||||
- 支持流式 SSE 双向转换(含 Tool Calling、Thinking)
|
||||
- 支持 Chat 核心层 + Models/Embeddings/Rerank 扩展层 + 未知接口透传
|
||||
- ProviderClient 支持多协议上游通信
|
||||
- 统一代理入口,URL 路由支持协议前缀
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 本阶段不实现多模态(Image/Audio/Video),Canonical Model 仅预留扩展点
|
||||
- 不实现 Middleware 的具体业务逻辑(仅定义接口和 Chain)
|
||||
- 不实现新的协议 Adapter(除 OpenAI 和 Anthropic 外)
|
||||
- 不实现有状态特性(架构预留 StatefulMiddleware 接口)
|
||||
- 不实现前端管理界面的协议选择功能
|
||||
- 不修改前端代码(前端使用管理 API,代理 API 路由变更对前端透明)
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: Canonical Model 用独立 Go 结构体实现,不使用 `interface{}` 或 `map[string]any`
|
||||
|
||||
**选择**:为 CanonicalRequest、CanonicalResponse、CanonicalStreamEvent 等定义强类型 Go struct,ContentBlock 使用 discriminated union 模式(type 字段 + 各类型嵌入)
|
||||
|
||||
**理由**:
|
||||
- 编译期类型安全,IDE 自动补全和重构友好
|
||||
- 性能优于 `map[string]any`(无反射开销)
|
||||
- 与 Go 生态的习惯一致
|
||||
|
||||
**替代方案**:
|
||||
- `map[string]any` — 灵活但无类型安全,重构时容易遗漏字段
|
||||
- 代码生成(如 protobuf)— 引入新依赖和构建步骤,过度工程化
|
||||
|
||||
**实现细节**:
|
||||
|
||||
```go
|
||||
type ContentBlock struct {
|
||||
Type string `json:"type"`
|
||||
// Text
|
||||
Text string `json:"text,omitempty"`
|
||||
// ToolUse
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Input json.RawMessage `json:"input,omitempty"`
|
||||
// ToolResult
|
||||
ToolUseID string `json:"tool_use_id,omitempty"`
|
||||
IsError *bool `json:"is_error,omitempty"`
|
||||
// Thinking
|
||||
Thinking string `json:"thinking,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
使用 `json.RawMessage` 保留 Tool Input 的原始 JSON,避免不必要的 `map` 解析。
|
||||
|
||||
### D2: ProtocolAdapter 接口集中定义所有方法,不用接口组合
|
||||
|
||||
**选择**:一个大的 `ProtocolAdapter` 接口包含所有方法(Chat、流式、扩展层、错误编码),不拆分为小接口
|
||||
|
||||
**理由**:
|
||||
- 对照 `docs/conversion_design.md` §5.2 的定义,接口集中便于明确所有应实现的内容
|
||||
- Adapter 实现者可一目了然看到所有方法
|
||||
- 不支持的功能直接返回 false(`supportsInterface`)或空实现
|
||||
- `detectInterfaceType` 由各协议 Adapter 实现,因为不同协议有不同的 URL 路径约定
|
||||
|
||||
**替代方案**:
|
||||
- 接口组合(ChatAdapter + StreamAdapter + ExtendedAdapter)——增加类型复杂度,Adapter 注册和管理更繁琐
|
||||
- 用空接口 + 类型断言——丢失编译期检查
|
||||
- `detectInterfaceType` 放在 ConversionEngine 中——违反开闭原则,新增协议需要修改 Engine
|
||||
|
||||
### D3: StreamDecoder 直接解析原始 SSE 字节流
|
||||
|
||||
**选择**:`ProviderClient.SendStream()` 返回 `<-chan []byte`(原始 SSE 字节流),`StreamDecoder.processChunk()` 负责拆分 SSE event 并解析 JSON
|
||||
|
||||
**理由**:
|
||||
- SSE 解析与协议语义紧密相关(不同协议的 SSE 格式不同:OpenAI 用 `data:` 无名事件,Anthropic 用命名事件 `event: xxx\ndata: xxx`)
|
||||
- 减少中间层,降低内存拷贝
|
||||
- ProviderClient 保持最简——只做 HTTP 请求和字节流读取
|
||||
|
||||
**替代方案**:
|
||||
- ProviderClient 内做 SSE 解析——强制所有上游使用同一 SSE 格式,不符合多协议目标
|
||||
- 独立 SSE Parser 层——增加不必要的抽象,SSE 格式本身就是 Adapter 职责的一部分
|
||||
|
||||
### D4: ProviderClient 接受 `HTTPRequestSpec`,返回 `*HTTPResponseSpec`
|
||||
|
||||
**选择**:ConversionEngine 输出 `HTTPRequestSpec{URL, Method, Headers, Body []byte}`,ProviderClient 接收后发送 HTTP 请求;响应返回 `HTTPResponseSpec{StatusCode, Headers, Body []byte}`
|
||||
|
||||
**理由**:
|
||||
- ProviderClient 完全不感知协议,只做 HTTP 通信
|
||||
- ConversionEngine 统一负责 URL 构建、Header 构建、Body 序列化
|
||||
- 同协议透传时,Engine 直接透传 body bytes,Client 不做任何序列化/反序列化
|
||||
|
||||
**接口定义**:
|
||||
|
||||
```go
|
||||
type HTTPRequestSpec struct {
|
||||
URL string
|
||||
Method string
|
||||
Headers map[string]string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
type HTTPResponseSpec struct {
|
||||
StatusCode int
|
||||
Headers map[string]string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
type ProviderClient interface {
|
||||
Send(ctx context.Context, spec HTTPRequestSpec) (*HTTPResponseSpec, error)
|
||||
SendStream(ctx context.Context, spec HTTPRequestSpec) (<-chan StreamEvent, error)
|
||||
}
|
||||
```
|
||||
|
||||
### D5: 统一代理入口使用 `/{protocol}/v1/...` URL 前缀
|
||||
|
||||
**选择**:新路由格式 `/{protocol}/v1/{path}`,handler 从 URL 提取 protocol 前缀作为 clientProtocol
|
||||
|
||||
**理由**:
|
||||
- 符合 `docs/conversion_design.md` §2.2 的设计
|
||||
- 调用方通过 URL 前缀明确指定协议,无需额外配置
|
||||
- 统一入口简化 handler 数量
|
||||
|
||||
**兼容路由**:不保留旧路由,客户端需迁移到新路由格式。
|
||||
|
||||
**替代方案**:
|
||||
- 保持两个独立 handler——违背统一架构目标
|
||||
- 请求体嗅探协议——不可靠,且设计文档明确"协议识别是调用方职责"
|
||||
|
||||
### D6: Provider 新增 `Protocol` 字段,存储在数据库
|
||||
|
||||
**选择**:Provider 表新增 `protocol TEXT DEFAULT 'openai'` 列,用于标识上游供应商使用的协议
|
||||
|
||||
**理由**:
|
||||
- 上游供应商可能是 OpenAI 兼容(大多数)或 Anthropic 原生
|
||||
- 路由时需要知道 providerProtocol 以选择正确的 Adapter
|
||||
- 默认值 `'openai'` 确保现有数据兼容
|
||||
|
||||
### D7: 删除旧 `internal/protocol/` 包,在 `internal/conversion/` 中重建
|
||||
|
||||
**选择**:直接删除 `internal/protocol/openai/` 和 `internal/protocol/anthropic/`,在 `internal/conversion/` 下从零构建新架构
|
||||
|
||||
**理由**:
|
||||
- 旧代码的设计模式(OpenAI 类型为枢纽)与新架构根本不同
|
||||
- 保留旧代码容易导致混用两种模式,引入隐蔽 bug
|
||||
- 旧代码中的类型定义可以迁移(copy-paste),但组织方式需重建
|
||||
|
||||
### D8: 目标包结构
|
||||
|
||||
```
|
||||
internal/conversion/
|
||||
canonical/
|
||||
types.go # CanonicalRequest/Response/Message/ContentBlock/Tool/ToolChoice/ThinkingConfig/OutputFormat
|
||||
stream.go # CanonicalStreamEvent 联合体 + 所有事件类型
|
||||
extended.go # CanonicalModelList/ModelInfo/Embedding/Rerank
|
||||
errors.go # ConversionError + ErrorCode 枚举
|
||||
interface.go # InterfaceType 枚举
|
||||
provider.go # TargetProvider struct
|
||||
adapter.go # ProtocolAdapter 接口 + AdapterRegistry 接口和实现
|
||||
stream.go # StreamDecoder/StreamEncoder/StreamConverter 接口 + Passthrough/Canonical 实现
|
||||
middleware.go # ConversionMiddleware 接口 + MiddlewareChain
|
||||
engine.go # ConversionEngine 门面 + HTTPRequestSpec/HTTPResponseSpec
|
||||
|
||||
openai/
|
||||
types.go # OpenAI 线路格式类型(从旧 protocol/openai/types.go 迁移并补全)
|
||||
adapter.go # ProtocolAdapter 实现(detectInterfaceType/buildUrl/buildHeaders/supportsInterface/encodeError)
|
||||
decoder.go # decodeRequest/decodeResponse/扩展层 decode 方法
|
||||
encoder.go # encodeRequest/encodeResponse/扩展层 encode 方法
|
||||
stream_decoder.go # OpenAIStreamDecoder(delta chunk 状态机)
|
||||
stream_encoder.go # OpenAIStreamEncoder(缓冲策略)
|
||||
|
||||
anthropic/
|
||||
types.go # Anthropic 线路格式类型(从旧 protocol/anthropic/types.go 迁移并补全)
|
||||
adapter.go # ProtocolAdapter 实现(detectInterfaceType/buildUrl/buildHeaders/supportsInterface/encodeError)
|
||||
decoder.go # decodeRequest/decodeResponse/扩展层 decode 方法
|
||||
encoder.go # encodeRequest/encodeResponse/扩展层 encode 方法
|
||||
stream_decoder.go # AnthropicStreamDecoder(命名事件 1:1 映射)
|
||||
stream_encoder.go # AnthropicStreamEncoder(直接映射,无缓冲)
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### R1: Anthropic 角色约束处理复杂度高
|
||||
**风险**:Anthropic 要求 user/assistant 严格交替、首消息必须为 user、tool_result 必须嵌入 user 消息。从 Canonical 编码为 Anthropic 时需要合并/拆分/注入消息,逻辑容易出错。
|
||||
**缓解**:
|
||||
- 编写详尽的测试用例覆盖所有边界情况(连续 tool 消息、首条 assistant 消息、空 user 消息注入等)
|
||||
- 将角色约束处理封装为独立函数,与内容编码逻辑分离
|
||||
|
||||
### R2: OpenAI 流式状态机复杂
|
||||
**风险**:OpenAI 的 delta chunk 没有显式生命周期(无 start/stop),StreamDecoder 需要状态机推断 block 边界,管理工具调用索引映射和参数累积。
|
||||
**缓解**:
|
||||
- 严格对照 `docs/conversion_openai.md` §6.2-§6.3 的伪代码实现
|
||||
- 为每种 delta 类型编写独立测试(text、tool_calls、reasoning_content、refusal、usage chunk)
|
||||
- UTF-8 跨 chunk 截断使用 `utf8Remainder` 缓冲
|
||||
|
||||
### R3: 全量重构影响范围大
|
||||
**风险**:同时删除旧代码、新建包、改造 handler/provider/domain,可能导致系统长时间不可用。
|
||||
**缓解**:
|
||||
- 旧代码在删除前确认新代码所有测试通过
|
||||
- Git 分支隔离开发,完成后再合并
|
||||
- 新路由 `/{protocol}/v1/...` 确保协议明确指定
|
||||
|
||||
### R4: Canonical Model 字段演进
|
||||
**风险**:Canonical Model 的字段集反映当前已适配协议的公共语义,未来新增协议时可能需要频繁修改。
|
||||
**缓解**:
|
||||
- 字段晋升规范已在 `docs/conversion_design.md` 附录 C 中定义
|
||||
- `json:"-"` 标签控制序列化输出,新增可选字段不影响已有编解码
|
||||
- 协议特有字段不纳入 Canonical,通过同协议透传保留
|
||||
|
||||
### R5: 性能——双重序列化开销
|
||||
**风险**:跨协议转换时经过 decode→canonical→encode 两次序列化/反序列化,相比直接转换多一次拷贝。
|
||||
**权衡**:接受此开销以换取架构清晰和可扩展性。同协议透传路径零开销补偿。实际 LLM API 延迟(数百毫秒到数秒)远大于 JSON 序列化开销(微秒级)。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### 步骤
|
||||
|
||||
1. **创建 `internal/conversion/` 包**:实现 Layer 1-3(Canonical Model、接口定义、Engine),不改动现有代码
|
||||
2. **实现 OpenAI Adapter 和 Anthropic Adapter**:Layer 4-5,在 conversion 包内自包含
|
||||
3. **编写全面测试**:覆盖编解码、流式转换、错误处理、同协议透传
|
||||
4. **改造 `domain.Provider`**:新增 `Protocol` 字段
|
||||
5. **创建数据库迁移**:`ALTER TABLE providers ADD COLUMN protocol TEXT DEFAULT 'openai'`
|
||||
6. **改造 `ProviderClient`**:简化为接受 `HTTPRequestSpec` 的 HTTP 发送器
|
||||
7. **创建 `ProxyHandler`**:统一代理入口,集成 ConversionEngine
|
||||
8. **更新 `cmd/server/main.go`**:注册 Adapter、创建 Engine、配置新路由
|
||||
9. **删除旧 `internal/protocol/` 包**:确认新架构完全替代后删除
|
||||
10. **更新 README.md**:项目结构、API 接口、路由说明
|
||||
|
||||
### 兼容策略
|
||||
|
||||
- 旧路由 `/v1/chat/completions` 和 `/v1/messages` 不再保留,客户端需迁移
|
||||
- 现有 Provider 数据通过 `DEFAULT 'openai'` 自动获得协议标识
|
||||
- 前端管理 API 不受影响
|
||||
|
||||
### 回滚策略
|
||||
|
||||
- Git 分支隔离:在新分支开发,合并前充分测试
|
||||
- 旧 `internal/protocol/` 包在确认新架构稳定后再删除
|
||||
- 数据库迁移向下兼容(仅 ADD COLUMN)
|
||||
|
||||
## Open Questions
|
||||
|
||||
- ~~是否需要为兼容路由 `/v1/chat/completions` 和 `/v1/messages` 设置 deprecation 期限?~~ → **决定**:不保留旧路由,客户端直接迁移到 `/{protocol}/v1/...`
|
||||
- ~~扩展层接口(Models/Embeddings/Rerank)在本阶段是否全部实现,还是先做 Models,其余后续迭代?~~ → **决定**:本阶段全部实现(对照三份文档的字段映射已在 spec 中完整定义),因为扩展层接口编解码逻辑量不大(轻量字段映射),且实现后能完整验证引擎的接口分层分发逻辑
|
||||
45
openspec/changes/refactor-conversion-engine/proposal.md
Normal file
45
openspec/changes/refactor-conversion-engine/proposal.md
Normal file
@@ -0,0 +1,45 @@
|
||||
## Why
|
||||
|
||||
当前后端协议转换层以 OpenAI 类型作为内部枢纽,Anthropic 请求单向转换为 OpenAI 格式后再发往上游。这种设计导致:无法支持 OpenAI→Anthropic 的反向转换、无法对接 Anthropic 协议的上游供应商、无法实现同协议透传的零开销转发、无法横向扩展新协议。重构为基于协议中立 Canonical Model 的 Hub-and-Spoke 架构(参考 `docs/conversion_design.md`),从根本上解决这些问题。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **引入 Canonical Model**:定义协议无关的 `CanonicalRequest`、`CanonicalResponse`、`CanonicalStreamEvent` 等规范模型,作为所有协议间转换的统一枢纽
|
||||
- **引入 ConversionEngine**:无状态的转换引擎门面,协调 Adapter 注册、接口识别、透传判断、请求/响应转换、流式转换
|
||||
- **引入 ProtocolAdapter 接口**:统一适配器契约,每种协议实现完整的编解码(Chat 请求/响应、流式、扩展层接口、错误编码)
|
||||
- **实现 OpenAI Adapter**:对照 `docs/conversion_openai.md` 实现 OpenAI 协议的完整 Adapter(含状态机流式解码器/编码器)
|
||||
- **实现 Anthropic Adapter**:对照 `docs/conversion_anthropic.md` 实现 Anthropic 协议的完整 Adapter(含命名事件流式解码器/编码器)
|
||||
- **统一代理 Handler**:合并 `OpenAIHandler` 和 `AnthropicHandler` 为统一的 `ProxyHandler`,支持 `/{protocol}/v1/...` URL 前缀路由
|
||||
- **同协议透传**:client == provider 时跳过 Canonical 转换,仅重建 Header 后原样转发
|
||||
- **接口分层**:核心层(Chat)走 Canonical 深度转换,扩展层(Models/Embeddings/Rerank)走轻量映射,未知接口走透传
|
||||
- **ProviderClient 简化**:移除 OpenAI Adapter 硬编码,变为协议无关的 HTTP 发送器
|
||||
- **Provider 新增 Protocol 字段**:**BREAKING** — Provider 模型新增 `protocol` 字段标识上游协议类型
|
||||
- **删除旧 protocol 包**:移除 `internal/protocol/openai/` 和 `internal/protocol/anthropic/`,全部逻辑迁入 `internal/conversion/`
|
||||
- **URL 路由变更**:**BREAKING** — 代理端点从 `/v1/chat/completions` + `/v1/messages` 变更为 `/{protocol}/v1/...`,不保留旧路由
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `conversion-engine`: 协议转换引擎核心能力——Canonical Model 定义、ProtocolAdapter 接口与注册表、ConversionEngine 门面(请求/响应转换、流式转换、接口识别、透传判断)、StreamDecoder/Encoder 接口、Middleware 拦截链、ConversionError 错误体系
|
||||
- `protocol-adapter-openai`: OpenAI 协议适配器——完整的 ProtocolAdapter 实现(对照 conversion_openai.md),涵盖 Chat 请求/响应编解码、流式状态机解码器(OpenAI delta chunk → CanonicalStreamEvent)和编码器(反向)、扩展层接口编解码(Models/Embeddings/Rerank)、错误编码、同协议透传
|
||||
- `protocol-adapter-anthropic`: Anthropic 协议适配器——完整的 ProtocolAdapter 实现(对照 conversion_anthropic.md),涵盖 Chat 请求/响应编解码(含角色约束处理:tool→user 合并、user/assistant 交替保证)、流式解码器(命名 SSE 事件 → CanonicalStreamEvent)和编码器(反向)、扩展层接口编解码(Models)、错误编码、同协议透传
|
||||
- `unified-proxy-handler`: 统一代理入口——合并 OpenAI/Anthropic 双 Handler 为统一 ProxyHandler,支持 `/{protocol}/v1/...` URL 前缀路由、协议识别
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `openai-protocol-proxy`: URL 路由从硬编码 `/v1/chat/completions` 变更为 `/{protocol}/v1/...` 统一入口;请求处理从直接调用 ProviderClient 变更为经 ConversionEngine 转换;新增同协议透传能力;新增扩展层接口(Models/Embeddings/Rerank)代理
|
||||
- `anthropic-protocol-proxy`: 从单向 Anthropic→OpenAI 转换变更为双向任意协议互转;从 Handler 内直接调用 converter 变更为经 ConversionEngine;新增 Anthropic 作为上游供应商的能力;新增同协议透传能力;新增扩展层接口代理
|
||||
- `provider-management`: Provider 模型新增 `protocol` 字段(标识上游协议类型,默认 "openai");数据库迁移新增 protocol 列
|
||||
- `layered-architecture`: 新增 conversion 层(internal/conversion/)位于 handler 和 provider 之间;ProviderClient 接口简化为协议无关的 HTTP 发送器
|
||||
- `error-handling`: 新增 ConversionError 错误类型和 ErrorCode 枚举;转换失败时使用客户端协议格式编码错误响应
|
||||
- `request-validation`: 请求验证从 handler 层前移到 ProtocolAdapter 的 decodeRequest 中;验证规则按各协议规范独立定义
|
||||
|
||||
## Impact
|
||||
|
||||
- **代码结构**:新增 `internal/conversion/` 包(约 20+ 文件),删除 `internal/protocol/` 包,改造 `internal/handler/` 和 `internal/provider/`
|
||||
- **API 兼容性**:**BREAKING** — 代理端点 URL 变更(`/v1/chat/completions` → `/openai/v1/chat/completions`,`/v1/messages` → `/anthropic/v1/messages`),不保留旧路由
|
||||
- **数据库**:Provider 表新增 `protocol` 列,需数据库迁移
|
||||
- **依赖**:无新增外部依赖,复用现有 Go 标准库和已引入的包
|
||||
- **测试**:需为 conversion 包编写全面单元测试,覆盖每个 Adapter 的编解码、流式转换、错误处理、同协议透传
|
||||
- **文档**:需更新 README.md 中的项目结构、API 接口说明
|
||||
@@ -0,0 +1,83 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 支持 Anthropic Messages API 端点
|
||||
|
||||
网关 SHALL 提供 Anthropic Messages API 端点供外部应用调用。
|
||||
|
||||
#### Scenario: 成功的非流式请求
|
||||
|
||||
- **WHEN** 应用发送 POST 请求到 `/anthropic/v1/messages`,携带有效的 Anthropic 请求格式(非流式)
|
||||
- **THEN** 网关 SHALL 通过 ConversionEngine 将 Anthropic 请求解码为 Canonical 格式
|
||||
- **THEN** 网关 SHALL 将 Canonical 请求编码为目标供应商协议格式
|
||||
- **THEN** 网关 SHALL 将供应商的响应通过 ConversionEngine 转换为 Anthropic 格式返回给应用
|
||||
|
||||
#### Scenario: 成功的流式请求
|
||||
|
||||
- **WHEN** 应用发送 POST 请求到 `/anthropic/v1/messages`,携带 `stream: true`
|
||||
- **THEN** 网关 SHALL 通过 ConversionEngine 创建 StreamConverter
|
||||
- **THEN** 网关 SHALL 将上游协议的 SSE 流转换为 Anthropic 命名事件格式
|
||||
- **THEN** 网关 SHALL 使用 `event: <type>\ndata: <json>\n\n` 格式流式返回给应用
|
||||
|
||||
#### Scenario: 同协议透传(Anthropic → Anthropic Provider)
|
||||
|
||||
- **WHEN** 客户端使用 Anthropic 协议且目标供应商也是 Anthropic 协议
|
||||
- **THEN** 网关 SHALL 跳过 Canonical 转换,仅重建认证 Header 后原样转发
|
||||
- **THEN** 请求和响应 Body SHALL 保持原样
|
||||
|
||||
### Requirement: 双向协议转换
|
||||
|
||||
网关 SHALL 支持 Anthropic 协议与任意已注册协议间的双向转换。
|
||||
|
||||
#### Scenario: Anthropic 客户端 → OpenAI 供应商
|
||||
|
||||
- **WHEN** 客户端使用 Anthropic 协议且供应商使用 OpenAI 协议
|
||||
- **THEN** SHALL 将 Anthropic MessagesRequest 解码为 CanonicalRequest
|
||||
- **THEN** SHALL 将 CanonicalRequest 编码为 OpenAI ChatCompletionRequest
|
||||
- **THEN** SHALL 将 OpenAI ChatCompletionResponse 解码为 CanonicalResponse
|
||||
- **THEN** SHALL 将 CanonicalResponse 编码为 Anthropic MessagesResponse
|
||||
|
||||
#### Scenario: OpenAI 客户端 → Anthropic 供应商
|
||||
|
||||
- **WHEN** 客户端使用 OpenAI 协议且供应商使用 Anthropic 协议
|
||||
- **THEN** SHALL 将 OpenAI ChatCompletionRequest 解码为 CanonicalRequest
|
||||
- **THEN** SHALL 将 CanonicalRequest 编码为 Anthropic MessagesRequest
|
||||
- **THEN** SHALL 将 Anthropic MessagesResponse 解码为 CanonicalResponse
|
||||
- **THEN** SHALL 将 CanonicalResponse 编码为 OpenAI ChatCompletionResponse
|
||||
|
||||
### Requirement: 使用 service 层处理请求
|
||||
|
||||
Handler SHALL 通过 service 层处理业务逻辑。
|
||||
|
||||
#### Scenario: 调用 routing service
|
||||
|
||||
- **WHEN** ProxyHandler 收到 Anthropic 协议请求
|
||||
- **THEN** SHALL 调用 RoutingService.Route() 获取路由结果
|
||||
- **THEN** SHALL 从路由结果获取 Provider(含 protocol 字段)
|
||||
|
||||
#### Scenario: 调用 stats service
|
||||
|
||||
- **WHEN** 请求成功完成
|
||||
- **THEN** SHALL 调用 StatsService.Record() 记录统计
|
||||
- **THEN** SHALL 异步记录统计(不阻塞响应)
|
||||
|
||||
### Requirement: 使用结构化错误处理
|
||||
|
||||
ProxyHandler SHALL 使用 ConversionError 和 Anthropic 的 encodeError 处理错误。
|
||||
|
||||
#### Scenario: 协议转换错误
|
||||
|
||||
- **WHEN** ConversionEngine 返回 ConversionError
|
||||
- **THEN** SHALL 使用 Anthropic 的 Adapter.encodeError 编码错误响应
|
||||
- **THEN** SHALL 使用 Anthropic 错误格式(`{type: "error", error: {type, message}}`)
|
||||
|
||||
#### Scenario: 路由错误处理
|
||||
|
||||
- **WHEN** RoutingService 返回错误
|
||||
- **THEN** SHALL 转换为 ConversionError
|
||||
- **THEN** SHALL 使用 Anthropic 错误格式返回
|
||||
|
||||
#### Scenario: 供应商错误处理
|
||||
|
||||
- **WHEN** ProviderClient 返回错误
|
||||
- **THEN** SHALL 包装为 ConversionError
|
||||
- **THEN** SHALL 使用 Anthropic 错误格式返回
|
||||
@@ -0,0 +1,276 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 定义 CanonicalRequest 规范模型
|
||||
|
||||
系统 SHALL 定义协议无关的 `CanonicalRequest` 结构体,作为所有协议间请求转换的统一枢纽。
|
||||
|
||||
CanonicalRequest SHALL 包含以下字段:
|
||||
- `model`: 字符串,模型名称
|
||||
- `system`: 可选,字符串或 SystemBlock 数组
|
||||
- `messages`: CanonicalMessage 数组
|
||||
- `tools`: 可选,CanonicalTool 数组
|
||||
- `tool_choice`: 可选,ToolChoice 联合体
|
||||
- `parameters`: RequestParameters
|
||||
- `thinking`: 可选,ThinkingConfig
|
||||
- `stream`: 布尔值
|
||||
- `user_id`: 可选字符串
|
||||
- `output_format`: 可选,OutputFormat
|
||||
- `parallel_tool_use`: 可选布尔值
|
||||
|
||||
#### Scenario: CanonicalRequest 包含所有公共字段
|
||||
|
||||
- **WHEN** 从任意协议解码请求为 CanonicalRequest
|
||||
- **THEN** CanonicalRequest SHALL 包含协议间可映射的所有公共语义字段
|
||||
- **THEN** 协议特有字段 SHALL NOT 出现在 CanonicalRequest 中
|
||||
|
||||
#### Scenario: System 消息独立存储
|
||||
|
||||
- **WHEN** 协议使用顶层 system 字段(如 Anthropic)
|
||||
- **THEN** SHALL 提取为 `canonical.system`
|
||||
- **WHEN** 协议使用 messages 数组中的 system 角色(如 OpenAI)
|
||||
- **THEN** SHALL 从 messages 中提取为 `canonical.system`
|
||||
|
||||
### Requirement: 定义 CanonicalMessage 和 ContentBlock
|
||||
|
||||
系统 SHALL 定义 `CanonicalMessage` 结构体和 `ContentBlock` 联合体。
|
||||
|
||||
CanonicalMessage SHALL 包含 `role`(枚举:system/user/assistant/tool)和 `content`(ContentBlock 数组)。
|
||||
|
||||
ContentBlock SHALL 支持以下类型:
|
||||
- `text`: 文本内容块
|
||||
- `tool_use`: 工具调用块(id, name, input)
|
||||
- `tool_result`: 工具结果块(tool_use_id, content, is_error)
|
||||
- `thinking`: 思考内容块(thinking)
|
||||
|
||||
#### Scenario: ContentBlock 类型化表示
|
||||
|
||||
- **WHEN** 编码 ContentBlock
|
||||
- **THEN** SHALL 使用 `type` 字段标识块类型
|
||||
- **THEN** 不同类型 SHALL 携带各自特有的字段
|
||||
|
||||
#### Scenario: Tool 输入保留原始 JSON
|
||||
|
||||
- **WHEN** 解码工具调用的 input 字段
|
||||
- **THEN** SHALL 使用 `json.RawMessage` 保留原始 JSON 数据
|
||||
- **THEN** SHALL NOT 强制解析为 map 或 struct
|
||||
|
||||
### Requirement: 定义 CanonicalTool 和 ToolChoice
|
||||
|
||||
系统 SHALL 定义 `CanonicalTool`(name, description, input_schema)和 `ToolChoice` 联合体(auto/none/any/tool+name)。
|
||||
|
||||
#### Scenario: ToolChoice 覆盖所有语义
|
||||
|
||||
- **WHEN** 协议使用 `"required"` 表示必须调用工具(如 OpenAI)
|
||||
- **THEN** SHALL 映射为 `{type: "any"}`
|
||||
- **WHEN** 协议使用 `{type: "tool", name}` 指定工具
|
||||
- **THEN** SHALL 保持 `{type: "tool", name}` 映射
|
||||
|
||||
### Requirement: 定义 CanonicalResponse 规范模型
|
||||
|
||||
系统 SHALL 定义 `CanonicalResponse` 结构体,包含 `id`、`model`、`content`(ContentBlock 数组)、`stop_reason`(可选枚举)、`usage`(CanonicalUsage)。
|
||||
|
||||
CanonicalUsage SHALL 包含 `input_tokens`、`output_tokens`、可选的 `cache_read_tokens`、`cache_creation_tokens`、`reasoning_tokens`。
|
||||
|
||||
stop_reason 枚举 SHALL 包含:`end_turn`、`max_tokens`、`tool_use`、`stop_sequence`、`content_filter`、`refusal`、`pause_turn`。
|
||||
|
||||
#### Scenario: 响应内容块与请求共用类型
|
||||
|
||||
- **WHEN** 编码响应中的 content
|
||||
- **THEN** SHALL 使用与请求相同的 ContentBlock 类型系统
|
||||
- **THEN** TextBlock 和 ToolUseBlock SHALL 在请求和响应中通用
|
||||
|
||||
### Requirement: 定义 CanonicalStreamEvent 联合体
|
||||
|
||||
系统 SHALL 定义类型化的 CanonicalStreamEvent 联合体,包含显式的 start/stop 生命周期。
|
||||
|
||||
事件类型 SHALL 包含:
|
||||
- `message_start`: 包含 message(id, model, usage)
|
||||
- `content_block_start`: 包含 index 和 content_block
|
||||
- `content_block_delta`: 包含 index 和 delta(text_delta/input_json_delta/thinking_delta)
|
||||
- `content_block_stop`: 包含 index
|
||||
- `message_delta`: 包含 delta(stop_reason)和 usage
|
||||
- `message_stop`: 无额外数据
|
||||
- `error`: 包含 error(type, message)
|
||||
- `ping`: 无额外数据
|
||||
|
||||
#### Scenario: 流式事件具有完整生命周期
|
||||
|
||||
- **WHEN** 解码协议的 SSE 流为 CanonicalStreamEvent
|
||||
- **THEN** SHALL 包含 message_start → content_block_start/delta/stop → message_delta → message_stop 的完整生命周期
|
||||
- **THEN** 每个事件 SHALL 包含足够的信息用于编码为任意目标协议的 SSE 格式
|
||||
|
||||
### Requirement: 定义 ProtocolAdapter 接口
|
||||
|
||||
系统 SHALL 定义 `ProtocolAdapter` 接口,每种协议实现完整适配。
|
||||
|
||||
ProtocolAdapter SHALL 包含以下方法:
|
||||
- `protocolName()`: 返回协议唯一标识
|
||||
- `protocolVersion()`: 返回协议版本
|
||||
- `supportsPassthrough()`: 返回是否支持同协议透传
|
||||
- `detectInterfaceType(nativePath)`: 根据协议的 URL 路径识别接口类型
|
||||
- `buildUrl(nativePath, interfaceType)`: 映射 URL 路径
|
||||
- `buildHeaders(provider)`: 构建认证和必要 Header
|
||||
- `supportsInterface(interfaceType)`: 判断是否支持某接口类型
|
||||
- `decodeRequest(raw)`: 协议格式 → CanonicalRequest
|
||||
- `encodeRequest(canonical, provider)`: CanonicalRequest → 协议格式
|
||||
- `decodeResponse(raw)`: 协议格式 → CanonicalResponse
|
||||
- `encodeResponse(canonical)`: CanonicalResponse → 协议格式
|
||||
- `createStreamDecoder()`: 创建 StreamDecoder 实例
|
||||
- `createStreamEncoder()`: 创建 StreamEncoder 实例
|
||||
- `encodeError(error)`: ConversionError → 协议错误格式
|
||||
- 扩展层编解码方法(Models/Embeddings/Rerank)
|
||||
|
||||
#### Scenario: Adapter 注册到 Registry
|
||||
|
||||
- **WHEN** 应用启动
|
||||
- **THEN** SHALL 将所有 Adapter 注册到 AdapterRegistry
|
||||
- **THEN** 可通过 `registry.get(protocolName)` 获取 Adapter
|
||||
|
||||
#### Scenario: Adapter 自描述接口能力
|
||||
|
||||
- **WHEN** 调用 `supportsInterface(interfaceType)`
|
||||
- **THEN** SHALL 返回布尔值表示是否支持该接口类型
|
||||
- **THEN** 不支持的接口 SHALL 由引擎走透传逻辑
|
||||
|
||||
#### Scenario: Adapter 识别接口类型
|
||||
|
||||
- **WHEN** 调用 `detectInterfaceType(nativePath)`
|
||||
- **THEN** SHALL 根据协议的 URL 路径模式识别接口类型
|
||||
- **THEN** SHALL 返回对应的 InterfaceType(CHAT/MODELS/MODEL_INFO/EMBEDDINGS/RERANK)
|
||||
- **THEN** 无法识别的路径 SHALL 返回 PASSTHROUGH 类型
|
||||
|
||||
### Requirement: 实现 AdapterRegistry
|
||||
|
||||
系统 SHALL 实现 `AdapterRegistry`,支持 Adapter 的注册和查询。
|
||||
|
||||
#### Scenario: 注册 Adapter
|
||||
|
||||
- **WHEN** 调用 `registry.register(adapter)`
|
||||
- **THEN** SHALL 以 adapter 的 protocolName 为 key 存储
|
||||
- **THEN** 重复注册同名协议 SHALL 返回错误
|
||||
|
||||
#### Scenario: 查询 Adapter
|
||||
|
||||
- **WHEN** 调用 `registry.get(protocolName)`
|
||||
- **THEN** SHALL 返回已注册的 ProtocolAdapter
|
||||
- **THEN** 未注册的协议 SHALL 返回错误
|
||||
|
||||
### Requirement: 实现 ConversionEngine 门面
|
||||
|
||||
系统 SHALL 实现 `ConversionEngine` 作为无状态的转换引擎门面,线程安全、可复用。
|
||||
|
||||
ConversionEngine SHALL 提供:
|
||||
- `registerAdapter(adapter)`: 注册协议适配器
|
||||
- `use(middleware)`: 注册转换中间件
|
||||
- `isPassthrough(clientProtocol, providerProtocol)`: 判断是否同协议透传
|
||||
- `convertHttpRequest(request, clientProtocol, providerProtocol, provider)`: 请求转换
|
||||
- `convertHttpResponse(response, clientProtocol, providerProtocol, interfaceType)`: 响应转换
|
||||
- `createStreamConverter(clientProtocol, providerProtocol, provider)`: 创建流式转换器
|
||||
|
||||
#### Scenario: 跨协议 Chat 请求转换
|
||||
|
||||
- **WHEN** clientProtocol != providerProtocol 且 interfaceType == CHAT
|
||||
- **THEN** SHALL 使用 clientAdapter.detectInterfaceType 识别接口类型
|
||||
- **THEN** SHALL 使用 clientAdapter.decodeRequest 解码为 CanonicalRequest
|
||||
- **THEN** SHALL 经 middlewareChain 处理
|
||||
- **THEN** SHALL 使用 providerAdapter.encodeRequest 编码为目标协议格式
|
||||
- **THEN** SHALL 使用 providerAdapter.buildUrl 映射目标 URL
|
||||
- **THEN** SHALL 使用 providerAdapter.buildHeaders 构建目标 Header
|
||||
- **THEN** SHALL 返回 HTTPRequestSpec{URL, Method, Headers, Body}
|
||||
|
||||
#### Scenario: 同协议透传
|
||||
|
||||
- **WHEN** clientProtocol == providerProtocol 且 supportsPassthrough == true
|
||||
- **THEN** SHALL 使用 providerAdapter.buildHeaders 重建 Header
|
||||
- **THEN** SHALL 原样传递请求 Body
|
||||
- **THEN** SHALL NOT 执行 decode/encode 转换
|
||||
|
||||
#### Scenario: 未知接口透传
|
||||
|
||||
- **WHEN** clientAdapter.detectInterfaceType 返回 PASSTHROUGH 类型
|
||||
- **THEN** SHALL 使用 providerAdapter.buildUrl 和 buildHeaders 适配 URL 和 Header
|
||||
- **THEN** SHALL 原样传递请求 Body
|
||||
|
||||
### Requirement: 定义 StreamDecoder/StreamEncoder/StreamConverter 接口
|
||||
|
||||
系统 SHALL 定义流式转换的三层接口。
|
||||
|
||||
StreamDecoder SHALL 接口:
|
||||
- `processChunk(rawChunk)`: 原始字节 → CanonicalStreamEvent 数组
|
||||
- `flush()`: 刷新缓冲区 → CanonicalStreamEvent 数组
|
||||
|
||||
StreamEncoder SHALL 接口:
|
||||
- `encodeEvent(event)`: CanonicalStreamEvent → SSE 字节数组
|
||||
- `flush()`: 刷新缓冲区 → SSE 字节数组
|
||||
|
||||
StreamConverter SHALL 接口:
|
||||
- `processChunk(rawChunk)`: 原始字节 → SSE 字节数组
|
||||
- `flush()`: 刷新 → SSE 字节数组
|
||||
|
||||
#### Scenario: PassthroughStreamConverter
|
||||
|
||||
- **WHEN** 同协议透传时
|
||||
- **THEN** SHALL 直接传递原始 SSE 字节,不做任何解析或转换
|
||||
|
||||
#### Scenario: CanonicalStreamConverter
|
||||
|
||||
- **WHEN** 跨协议转换时
|
||||
- **THEN** SHALL 使用 providerAdapter 的 StreamDecoder 解码原始 SSE
|
||||
- **THEN** SHALL 经 middlewareChain 处理事件
|
||||
- **THEN** SHALL 使用 clientAdapter 的 StreamEncoder 编码为目标 SSE 格式
|
||||
|
||||
### Requirement: 定义 ConversionMiddleware 接口
|
||||
|
||||
系统 SHALL 定义 `ConversionMiddleware` 接口,用于在 decode→encode 之间拦截转换。
|
||||
|
||||
- `intercept(canonical, clientProtocol, providerProtocol, context)`: 修改或拒绝 Canonical
|
||||
- `interceptStreamEvent(event, clientProtocol, providerProtocol, context)`: 修改或拒绝流式事件
|
||||
|
||||
#### Scenario: Middleware 链式执行
|
||||
|
||||
- **WHEN** 注册多个 Middleware
|
||||
- **THEN** SHALL 按注册顺序链式执行
|
||||
- **THEN** 任一 Middleware 返回错误 SHALL 中断后续执行
|
||||
|
||||
#### Scenario: Middleware 中断转换
|
||||
|
||||
- **WHEN** Middleware 返回 ConversionError
|
||||
- **THEN** SHALL 停止转换流程
|
||||
- **THEN** SHALL 使用 clientAdapter.encodeError 编码错误响应
|
||||
|
||||
### Requirement: 定义 ConversionError 错误体系
|
||||
|
||||
系统 SHALL 定义 `ConversionError` 和 `ErrorCode` 枚举。
|
||||
|
||||
ErrorCode SHALL 包含:INVALID_INPUT、MISSING_REQUIRED_FIELD、INCOMPATIBLE_FEATURE、FIELD_MAPPING_FAILURE、TOOL_CALL_PARSE_ERROR、JSON_PARSE_ERROR、STREAM_STATE_ERROR、UTF8_DECODE_ERROR、PROTOCOL_CONSTRAINT_VIOLATION、ENCODING_FAILURE、INTERFACE_NOT_SUPPORTED。
|
||||
|
||||
#### Scenario: 转换错误包含上下文
|
||||
|
||||
- **WHEN** 转换过程中发生错误
|
||||
- **THEN** ConversionError SHALL 包含 code、message、clientProtocol、providerProtocol、interfaceType 等上下文
|
||||
- **THEN** SHALL 支持包装原始错误(cause)
|
||||
|
||||
### Requirement: 定义 InterfaceType 枚举和接口分层
|
||||
|
||||
系统 SHALL 定义 `InterfaceType` 枚举(CHAT、MODELS、MODEL_INFO、EMBEDDINGS、RERANK)和接口分层策略。
|
||||
|
||||
- 核心层(CHAT):使用 Canonical Model 深度转换
|
||||
- 扩展层(MODELS、MODEL_INFO、EMBEDDINGS、RERANK):使用轻量 Canonical Models 做字段映射
|
||||
- 透传层(未知接口):URL+Header 适配后 Body 原样转发
|
||||
|
||||
#### Scenario: 扩展层接口转换
|
||||
|
||||
- **WHEN** interfaceType 为 MODELS/MODEL_INFO/EMBEDDINGS/RERANK
|
||||
- **THEN** SHALL 使用对应扩展层 Canonical Model 做轻量字段映射
|
||||
- **THEN** 双方都不支持时 SHALL 走透传逻辑
|
||||
|
||||
### Requirement: 定义 TargetProvider 结构体
|
||||
|
||||
系统 SHALL 定义 `TargetProvider` 结构体,包含 `base_url`、`api_key`、`model_name`、`adapter_config`。
|
||||
|
||||
#### Scenario: Adapter 从 TargetProvider 获取配置
|
||||
|
||||
- **WHEN** Adapter 调用 buildHeaders(provider)
|
||||
- **THEN** SHALL 从 provider.api_key 提取认证信息
|
||||
- **THEN** SHALL 从 provider.adapter_config 提取协议专属配置
|
||||
- **THEN** SHALL 使用 provider.model_name 覆盖请求中的 model 字段
|
||||
@@ -0,0 +1,53 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 统一错误响应
|
||||
|
||||
系统 SHALL 统一错误响应格式,新增 ConversionError 支持。
|
||||
|
||||
#### Scenario: OpenAI 协议错误响应
|
||||
|
||||
- **WHEN** OpenAI 协议发生错误
|
||||
- **THEN** SHALL 返回标准 OpenAI 错误响应格式
|
||||
- **THEN** SHALL 包含 error.message、error.type、error.code 字段
|
||||
|
||||
#### Scenario: Anthropic 协议错误响应
|
||||
|
||||
- **WHEN** Anthropic 协议发生错误
|
||||
- **THEN** SHALL 返回标准 Anthropic 错误响应格式
|
||||
- **THEN** SHALL 包含 type、error.type、error.message 字段
|
||||
|
||||
#### Scenario: 转换错误响应
|
||||
|
||||
- **WHEN** ConversionEngine 在协议转换过程中产生 ConversionError
|
||||
- **THEN** SHALL 使用客户端协议的 Adapter.encodeError 编码错误响应
|
||||
- **THEN** 错误响应 SHALL 使用客户端可理解的协议格式
|
||||
|
||||
#### Scenario: 管理 API 错误响应
|
||||
|
||||
- **WHEN** 管理 API 发生错误
|
||||
- **THEN** SHALL 返回统一的错误响应格式
|
||||
- **THEN** SHALL 包含 code、message 字段
|
||||
- **THEN** SHALL 可选包含 details 字段(验证错误详情)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 定义 ConversionError 错误类型
|
||||
|
||||
系统 SHALL 定义 ConversionError 结构体和 ErrorCode 枚举。
|
||||
|
||||
#### Scenario: ConversionError 结构
|
||||
|
||||
- **WHEN** 定义转换错误
|
||||
- **THEN** SHALL 包含 Code(ErrorCode 枚举)、Message 字段
|
||||
- **THEN** SHALL 可选包含 ClientProtocol、ProviderProtocol、InterfaceType、Details、Cause 字段
|
||||
|
||||
#### Scenario: ErrorCode 枚举
|
||||
|
||||
- **WHEN** 定义错误码
|
||||
- **THEN** SHALL 包含 INVALID_INPUT、MISSING_REQUIRED_FIELD、INCOMPATIBLE_FEATURE、FIELD_MAPPING_FAILURE、TOOL_CALL_PARSE_ERROR、JSON_PARSE_ERROR、STREAM_STATE_ERROR、UTF8_DECODE_ERROR、PROTOCOL_CONSTRAINT_VIOLATION、ENCODING_FAILURE、INTERFACE_NOT_SUPPORTED
|
||||
|
||||
#### Scenario: 错误码到协议错误类型的映射
|
||||
|
||||
- **WHEN** 使用 encodeError 编码错误
|
||||
- **THEN** ErrorCode SHALL 映射为各协议的错误类型字符串
|
||||
- **THEN** 例如 INVALID_INPUT → OpenAI "invalid_request_error",Anthropic "invalid_request_error"
|
||||
@@ -0,0 +1,118 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 实现三层架构
|
||||
|
||||
系统 SHALL 实现 handler → service → repository 三层架构,并在 handler 和 provider 之间新增 conversion 层。
|
||||
|
||||
#### Scenario: Handler 层职责
|
||||
|
||||
- **WHEN** 处理 HTTP 请求
|
||||
- **THEN** handler 层 SHALL 仅负责 HTTP 请求解析、URL 路由和响应写入
|
||||
- **THEN** handler 层 SHALL 调用 ConversionEngine 处理协议转换
|
||||
- **THEN** handler 层 SHALL 调用 service 层处理业务逻辑
|
||||
- **THEN** handler 层 SHALL NOT 直接访问数据库或执行协议转换逻辑
|
||||
|
||||
#### Scenario: Conversion 层职责
|
||||
|
||||
- **WHEN** 处理协议转换
|
||||
- **THEN** conversion 层 SHALL 包含 Canonical Model 定义
|
||||
- **THEN** conversion 层 SHALL 包含各协议的 ProtocolAdapter 实现
|
||||
- **THEN** conversion 层 SHALL 包含 ConversionEngine 门面
|
||||
- **THEN** conversion 层 SHALL NOT 依赖 handler 或 service 层
|
||||
|
||||
#### Scenario: Service 层职责
|
||||
|
||||
- **WHEN** 处理业务逻辑
|
||||
- **THEN** service 层 SHALL 包含业务规则和验证
|
||||
- **THEN** service 层 SHALL 调用 repository 层访问数据
|
||||
- **THEN** service 层 SHALL NOT 包含协议转换逻辑
|
||||
|
||||
#### Scenario: Repository 层职责
|
||||
|
||||
- **WHEN** 访问数据
|
||||
- **THEN** repository 层 SHALL 仅负责数据访问
|
||||
- **THEN** repository 层 SHALL 封装数据库操作
|
||||
- **THEN** repository 层 SHALL NOT 包含业务逻辑或协议转换逻辑
|
||||
|
||||
### Requirement: 定义核心接口
|
||||
|
||||
系统 SHALL 定义清晰的接口边界。
|
||||
|
||||
#### Scenario: Service 接口定义
|
||||
|
||||
- **WHEN** 定义 service 接口
|
||||
- **THEN** SHALL 定义 ProviderService、ModelService、RoutingService、StatsService 接口
|
||||
- **THEN** SHALL 定义清晰的业务方法签名
|
||||
- **THEN** SHALL 使用 domain 类型作为参数和返回值
|
||||
|
||||
#### Scenario: Repository 接口定义
|
||||
|
||||
- **WHEN** 定义 repository 接口
|
||||
- **THEN** SHALL 定义 ProviderRepository、ModelRepository、StatsRepository 接口
|
||||
- **THEN** SHALL 定义清晰的数据访问方法签名
|
||||
- **THEN** SHALL 使用 domain 类型作为参数和返回值
|
||||
|
||||
#### Scenario: Provider Client 接口定义
|
||||
|
||||
- **WHEN** 定义 provider client 接口
|
||||
- **THEN** SHALL 定义 ProviderClient 接口
|
||||
- **THEN** SHALL 包含 Send(非流式)和 SendStream(流式)方法
|
||||
- **THEN** SHALL 接受 HTTPRequestSpec 作为参数,不绑定特定协议
|
||||
- **THEN** SHALL 支持接口 Mock
|
||||
|
||||
#### Scenario: Conversion 层接口定义
|
||||
|
||||
- **WHEN** 定义 conversion 层接口
|
||||
- **THEN** SHALL 定义 ProtocolAdapter、StreamDecoder、StreamEncoder、StreamConverter、ConversionMiddleware 接口
|
||||
- **THEN** SHALL 定义 AdapterRegistry 用于 Adapter 注册和查询
|
||||
- **THEN** SHALL 定义 ConversionEngine 作为统一门面
|
||||
|
||||
### Requirement: 实现依赖注入
|
||||
|
||||
系统 SHALL 使用手动依赖注入。
|
||||
|
||||
#### Scenario: Repository 注入
|
||||
|
||||
- **WHEN** 初始化 service
|
||||
- **THEN** SHALL 通过构造函数注入 repository 依赖
|
||||
- **THEN** SHALL 使用接口类型而非具体类型
|
||||
|
||||
#### Scenario: Service 注入
|
||||
|
||||
- **WHEN** 初始化 handler
|
||||
- **THEN** SHALL 通过构造函数注入 service 依赖、ConversionEngine、ProviderClient
|
||||
- **THEN** SHALL 使用接口类型而非具体类型
|
||||
|
||||
#### Scenario: Conversion 组装
|
||||
|
||||
- **WHEN** 应用启动
|
||||
- **THEN** SHALL 创建 AdapterRegistry 并注册所有 ProtocolAdapter
|
||||
- **THEN** SHALL 创建 ConversionEngine(注入 registry 和 middleware chain)
|
||||
- **THEN** SHALL 将 ConversionEngine 注入到 ProxyHandler
|
||||
|
||||
#### Scenario: 主函数组装
|
||||
|
||||
- **WHEN** 应用启动
|
||||
- **THEN** main.go SHALL 按顺序构造所有依赖
|
||||
- **THEN** SHALL 先构造基础设施(logger、database)
|
||||
- **THEN** SHALL 再构造 repository、service
|
||||
- **THEN** SHALL 再构造 conversion 层(registry → engine)
|
||||
- **THEN** SHALL 最后构造 handler
|
||||
|
||||
### Requirement: 定义 Domain 模型
|
||||
|
||||
系统 SHALL 定义独立的 domain 模型。
|
||||
|
||||
#### Scenario: Domain 模型定义
|
||||
|
||||
- **WHEN** 定义领域模型
|
||||
- **THEN** SHALL 在 internal/domain/ 包中定义
|
||||
- **THEN** SHALL 包含 Provider、Model、UsageStats 等模型
|
||||
- **THEN** Provider SHALL 包含 Protocol 字段
|
||||
- **THEN** SHALL 与数据库模型分离
|
||||
|
||||
#### Scenario: Domain 模型使用
|
||||
|
||||
- **WHEN** service 和 repository 处理数据
|
||||
- **THEN** SHALL 使用 domain 模型
|
||||
- **THEN** SHALL NOT 使用数据库模型(GORM 模型)
|
||||
@@ -0,0 +1,99 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 支持 OpenAI Chat Completions API 端点
|
||||
|
||||
网关 SHALL 提供 OpenAI Chat Completions API 端点供外部应用调用。
|
||||
|
||||
#### Scenario: 成功的非流式请求
|
||||
|
||||
- **WHEN** 应用发送 POST 请求到 `/openai/v1/chat/completions`,携带有效的 OpenAI 请求格式(非流式)
|
||||
- **THEN** 网关 SHALL 通过 ConversionEngine 转换请求
|
||||
- **THEN** 网关 SHALL 将转换后的请求转发到配置的供应商
|
||||
- **THEN** 网关 SHALL 将供应商的响应通过 ConversionEngine 转换为 OpenAI 格式返回给应用
|
||||
|
||||
#### Scenario: 成功的流式请求
|
||||
|
||||
- **WHEN** 应用发送 POST 请求到 `/openai/v1/chat/completions`,携带 `stream: true`
|
||||
- **THEN** 网关 SHALL 通过 ConversionEngine 创建 StreamConverter
|
||||
- **THEN** 网关 SHALL 使用 SSE 格式将转换后的响应流式返回给应用
|
||||
- **THEN** 网关 SHALL 在流完成时发送 `data: [DONE]`
|
||||
|
||||
#### Scenario: 同协议透传(OpenAI → OpenAI Provider)
|
||||
|
||||
- **WHEN** 客户端使用 OpenAI 协议且目标供应商也是 OpenAI 协议
|
||||
- **THEN** 网关 SHALL 跳过 Canonical 转换,仅重建认证 Header 后原样转发
|
||||
- **THEN** 请求和响应 Body SHALL 保持原样
|
||||
|
||||
### Requirement: 根据模型名称路由请求
|
||||
|
||||
网关 SHALL 根据请求中的 `model` 字段将请求路由到相应的供应商。
|
||||
|
||||
#### Scenario: 有效模型路由
|
||||
|
||||
- **WHEN** 请求包含存在于配置模型中的 `model` 字段
|
||||
- **AND** 该模型已启用
|
||||
- **THEN** 网关 SHALL 将请求路由到该模型关联的供应商
|
||||
- **THEN** 网关 SHALL 从供应商的 `protocol` 字段获取 providerProtocol
|
||||
|
||||
#### Scenario: 模型未找到
|
||||
|
||||
- **WHEN** 请求包含不存在于配置模型中的 `model` 字段
|
||||
- **THEN** 网关 SHALL 使用 OpenAI 格式返回错误响应
|
||||
|
||||
#### Scenario: 模型已禁用
|
||||
|
||||
- **WHEN** 请求包含已禁用模型的 `model` 字段
|
||||
- **THEN** 网关 SHALL 使用 OpenAI 格式返回错误响应
|
||||
|
||||
### Requirement: 对 OpenAI 兼容供应商透明代理
|
||||
|
||||
网关 SHALL 对 OpenAI 兼容供应商的请求和响应通过 ConversionEngine 进行转换处理。
|
||||
|
||||
#### Scenario: 跨协议请求转发
|
||||
|
||||
- **WHEN** 网关收到 OpenAI 协议请求且目标供应商使用不同协议
|
||||
- **THEN** 网关 SHALL 通过 ConversionEngine 将请求转换为目标协议格式
|
||||
- **THEN** 网关 SHALL 使用目标协议的 Adapter 构建 URL 和 Header
|
||||
|
||||
#### Scenario: 扩展层接口代理
|
||||
|
||||
- **WHEN** 网关收到 `/openai/v1/models` 等 GET 请求
|
||||
- **THEN** 网关 SHALL 通过 ConversionEngine 转换扩展层接口的响应格式
|
||||
|
||||
### Requirement: 使用 service 层处理请求
|
||||
|
||||
Handler SHALL 通过 service 层处理业务逻辑。
|
||||
|
||||
#### Scenario: 调用 routing service
|
||||
|
||||
- **WHEN** ProxyHandler 收到请求
|
||||
- **THEN** SHALL 调用 RoutingService.Route() 获取路由结果
|
||||
- **THEN** SHALL 从路由结果获取 Provider(含 protocol 字段)
|
||||
|
||||
#### Scenario: 调用 stats service
|
||||
|
||||
- **WHEN** 请求成功完成
|
||||
- **THEN** SHALL 调用 StatsService.Record() 记录统计
|
||||
- **THEN** SHALL 异步记录统计(不阻塞响应)
|
||||
|
||||
### Requirement: 使用结构化错误处理
|
||||
|
||||
ProxyHandler SHALL 使用 ConversionError 和协议对应的 encodeError 处理错误。
|
||||
|
||||
#### Scenario: 转换错误
|
||||
|
||||
- **WHEN** ConversionEngine 返回 ConversionError
|
||||
- **THEN** SHALL 使用 clientProtocol 的 Adapter.encodeError 编码错误响应
|
||||
- **THEN** SHALL 使用 OpenAI 错误格式(`{error: {message, type, code}}`)
|
||||
|
||||
#### Scenario: 路由错误处理
|
||||
|
||||
- **WHEN** RoutingService 返回错误
|
||||
- **THEN** SHALL 转换为 ConversionError
|
||||
- **THEN** SHALL 使用 OpenAI 错误格式返回
|
||||
|
||||
#### Scenario: 供应商错误处理
|
||||
|
||||
- **WHEN** ProviderClient 返回错误
|
||||
- **THEN** SHALL 包装为 ConversionError
|
||||
- **THEN** SHALL 使用 OpenAI 错误格式返回
|
||||
@@ -0,0 +1,269 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 实现 Anthropic ProtocolAdapter
|
||||
|
||||
系统 SHALL 实现 Anthropic 协议的完整 ProtocolAdapter,对照 `docs/conversion_anthropic.md`。
|
||||
|
||||
- `protocolName()` SHALL 返回 `"anthropic"`
|
||||
- `supportsPassthrough()` SHALL 返回 true
|
||||
- `buildHeaders(provider)` SHALL 构建 `x-api-key`、`anthropic-version`、`anthropic-beta`、`Content-Type`
|
||||
- `buildUrl(nativePath, interfaceType)` SHALL 按接口类型映射 URL 路径
|
||||
- `supportsInterface()` SHALL 对 CHAT、MODELS、MODEL_INFO 返回 true,对 EMBEDDINGS、RERANK 返回 false
|
||||
|
||||
#### Scenario: 认证 Header 构建
|
||||
|
||||
- **WHEN** 调用 buildHeaders(provider)
|
||||
- **THEN** SHALL 设置 `x-api-key: <provider.api_key>`
|
||||
- **THEN** SHALL 设置 `anthropic-version`(默认 `"2023-06-01"`,从 adapter_config 可覆盖)
|
||||
- **WHEN** adapter_config 包含 anthropic_beta
|
||||
- **THEN** SHALL 以逗号拼接为 `anthropic-beta` Header
|
||||
|
||||
#### Scenario: URL 映射
|
||||
|
||||
- **WHEN** interfaceType == CHAT
|
||||
- **THEN** SHALL 映射为 `/v1/messages`
|
||||
- **WHEN** interfaceType == MODELS
|
||||
- **THEN** SHALL 映射为 `/v1/models`
|
||||
- **WHEN** interfaceType == EMBEDDINGS 或 RERANK
|
||||
- **THEN** SHALL NOT 调用 buildUrl(supportsInterface 返回 false,引擎走透传)
|
||||
|
||||
### Requirement: Anthropic 请求解码(Anthropic → Canonical)
|
||||
|
||||
系统 SHALL 实现完整的 Anthropic MessagesRequest 到 CanonicalRequest 的解码。
|
||||
|
||||
#### Scenario: System 消息提取
|
||||
|
||||
- **WHEN** Anthropic 请求包含顶层 `system` 字段
|
||||
- **THEN** String 类型 SHALL 直接提取为 canonical.system
|
||||
- **THEN** SystemBlock 数组 SHALL 提取为 canonical.system(Array)
|
||||
|
||||
#### Scenario: User 消息中 tool_result 拆分
|
||||
|
||||
- **WHEN** Anthropic user 消息的 content 中包含 tool_result 块
|
||||
- **THEN** SHALL 将 tool_result 块拆分为独立的 CanonicalMessage{role: "tool"}
|
||||
- **THEN** 非 tool_result 块 SHALL 保留为独立的 CanonicalMessage{role: "user"}
|
||||
- **THEN** 仅 tool_result 块时 SHALL 只产出 tool 角色消息
|
||||
|
||||
#### Scenario: 参数映射
|
||||
|
||||
- **WHEN** 解码 Anthropic 请求参数
|
||||
- **THEN** max_tokens SHALL 直接映射
|
||||
- **THEN** temperature/top_p/top_k SHALL 直接映射
|
||||
- **THEN** stop_sequences SHALL 直接映射
|
||||
|
||||
#### Scenario: ThinkingConfig 解码
|
||||
|
||||
- **WHEN** 解码 Anthropic thinking 字段
|
||||
- **THEN** type="enabled" SHALL 映射为 Canonical thinking.type="enabled"
|
||||
- **THEN** type="disabled" SHALL 映射为 Canonical thinking.type="disabled"
|
||||
- **THEN** type="adaptive" SHALL 映射为 Canonical thinking.type="adaptive"
|
||||
- **THEN** budget_tokens 和 output_config.effort SHALL 直接映射
|
||||
|
||||
#### Scenario: 公共字段提取
|
||||
|
||||
- **WHEN** 解码 Anthropic 公共字段
|
||||
- **THEN** metadata.user_id SHALL 提取为 user_id
|
||||
- **THEN** output_config.format(json_schema 类型)SHALL 提取为 output_format
|
||||
- **THEN** disable_parallel_tool_use SHALL 反转映射为 parallel_tool_use(true → false)
|
||||
|
||||
#### Scenario: 协议特有字段处理
|
||||
|
||||
- **WHEN** 解码遇到 redacted_thinking
|
||||
- **THEN** SHALL 丢弃,不在中间层保留
|
||||
- **WHEN** 解码遇到 cache_control
|
||||
- **THEN** SHALL 忽略,不晋升为公共字段
|
||||
|
||||
### Requirement: Anthropic 请求编码(Canonical → Anthropic)
|
||||
|
||||
系统 SHALL 实现完整的 CanonicalRequest 到 Anthropic MessagesRequest 的编码。
|
||||
|
||||
#### Scenario: System 消息注入
|
||||
|
||||
- **WHEN** canonical.system 不为空
|
||||
- **THEN** SHALL 编码为 Anthropic 顶层 `system` 字段
|
||||
|
||||
#### Scenario: Tool 角色合并到 User 消息
|
||||
|
||||
- **WHEN** CanonicalMessage{role: "tool"} 出现在消息序列中
|
||||
- **THEN** SHALL 将其 tool_result 块合并到相邻的 Anthropic user 消息的 content 数组中
|
||||
- **WHEN** 相邻前一条不是 user 消息
|
||||
- **THEN** SHALL 创建新的 user 消息来承载 tool_result 块
|
||||
|
||||
#### Scenario: 首消息 user 保证
|
||||
|
||||
- **WHEN** 编码后的 Anthropic messages 数组首条消息不是 user 角色
|
||||
- **THEN** SHALL 自动注入一条空 user 消息到头部
|
||||
|
||||
#### Scenario: 角色交替约束
|
||||
|
||||
- **WHEN** 编码后存在连续同角色消息
|
||||
- **THEN** SHALL 合并为单条消息(content 数组拼接)
|
||||
|
||||
#### Scenario: 参数编码
|
||||
|
||||
- **WHEN** 编码 CanonicalRequest 参数
|
||||
- **THEN** parameters.max_tokens SHALL 直接映射(Anthropic 必填)
|
||||
- **THEN** parameters.top_k SHALL 直接映射
|
||||
- **THEN** canonical.thinking.type="enabled" SHALL 映射为 thinking{type: "enabled", budget_tokens}
|
||||
- **THEN** canonical.thinking.type="adaptive" SHALL 映射为 thinking{type: "adaptive"}
|
||||
|
||||
#### Scenario: 公共字段编码
|
||||
|
||||
- **WHEN** canonical.user_id 不为空
|
||||
- **THEN** SHALL 编码为 metadata.user_id
|
||||
- **WHEN** canonical.parallel_tool_use == false
|
||||
- **THEN** SHALL 编码为 disable_parallel_tool_use: true
|
||||
- **WHEN** canonical.output_format 存在
|
||||
- **THEN** SHALL 编码为 output_config.format
|
||||
|
||||
#### Scenario: 降级处理
|
||||
|
||||
- **WHEN** canonical.output_format.type == "json_object"
|
||||
- **THEN** SHALL 降级为 output_config.format{type: "json_schema", schema: {type: "object"}}
|
||||
- **WHEN** canonical.output_format.type == "text"
|
||||
- **THEN** SHALL 丢弃,不设置 output_config
|
||||
|
||||
### Requirement: Anthropic 响应解码(Anthropic → Canonical)
|
||||
|
||||
系统 SHALL 实现 Anthropic MessagesResponse 到 CanonicalResponse 的解码。
|
||||
|
||||
#### Scenario: 内容块解码
|
||||
|
||||
- **WHEN** Anthropic response 包含 text 块
|
||||
- **THEN** SHALL 解码为 TextBlock
|
||||
- **WHEN** 包含 tool_use 块
|
||||
- **THEN** SHALL 解码为 ToolUseBlock
|
||||
- **WHEN** 包含 thinking 块
|
||||
- **THEN** SHALL 解码为 ThinkingBlock
|
||||
- **WHEN** 包含 redacted_thinking 块
|
||||
- **THEN** SHALL 丢弃
|
||||
|
||||
#### Scenario: 停止原因映射
|
||||
|
||||
- **WHEN** 解码 stop_reason
|
||||
- **THEN** "end_turn" SHALL 映射为 "end_turn"
|
||||
- **THEN** "max_tokens" SHALL 映射为 "max_tokens"
|
||||
- **THEN** "tool_use" SHALL 映射为 "tool_use"
|
||||
- **THEN** "stop_sequence" SHALL 映射为 "stop_sequence"
|
||||
- **THEN** "refusal" SHALL 映射为 "refusal"
|
||||
- **THEN** "pause_turn" SHALL 映射为 "pause_turn"
|
||||
|
||||
#### Scenario: Usage 映射
|
||||
|
||||
- **WHEN** 解码 Anthropic usage
|
||||
- **THEN** input_tokens SHALL 直接映射
|
||||
- **THEN** output_tokens SHALL 直接映射
|
||||
- **THEN** cache_read_input_tokens SHALL 映射为 cache_read_tokens
|
||||
- **THEN** cache_creation_input_tokens SHALL 映射为 cache_creation_tokens
|
||||
|
||||
### Requirement: Anthropic 响应编码(Canonical → Anthropic)
|
||||
|
||||
系统 SHALL 实现 CanonicalResponse 到 Anthropic MessagesResponse 的编码。
|
||||
|
||||
#### Scenario: 降级处理
|
||||
|
||||
- **WHEN** canonical.stop_reason 为 "content_filter"
|
||||
- **THEN** SHALL 降级映射为 "end_turn"
|
||||
- **WHEN** canonical.reasoning_tokens 不为空
|
||||
- **THEN** SHALL 丢弃(Anthropic 无此字段)
|
||||
|
||||
### Requirement: Anthropic 流式解码器
|
||||
|
||||
系统 SHALL 实现 AnthropicStreamDecoder,将 Anthropic 命名 SSE 事件转换为 CanonicalStreamEvent。
|
||||
|
||||
Decoder 几乎 1:1 映射,维护最小状态机:
|
||||
- messageStarted: 是否已发送 MessageStartEvent
|
||||
- redactedBlocks: 需要丢弃的 block index 集合
|
||||
- utf8Remainder: UTF-8 跨 chunk 安全缓冲
|
||||
|
||||
#### Scenario: 命名事件 1:1 映射
|
||||
|
||||
- **WHEN** 收到 `event: message_start`
|
||||
- **THEN** SHALL 发出 MessageStartEvent
|
||||
- **WHEN** 收到 `event: content_block_start`
|
||||
- **THEN** SHALL 发出 ContentBlockStartEvent
|
||||
- **WHEN** 收到 `event: content_block_delta`
|
||||
- **THEN** SHALL 发出 ContentBlockDeltaEvent
|
||||
- **WHEN** 收到 `event: content_block_stop`
|
||||
- **THEN** SHALL 发出 ContentBlockStopEvent
|
||||
- **WHEN** 收到 `event: message_delta`
|
||||
- **THEN** SHALL 发出 MessageDeltaEvent
|
||||
- **WHEN** 收到 `event: message_stop`
|
||||
- **THEN** SHALL 发出 MessageStopEvent
|
||||
- **WHEN** 收到 `event: ping`
|
||||
- **THEN** SHALL 发出 PingEvent
|
||||
- **WHEN** 收到 `event: error`
|
||||
- **THEN** SHALL 发出 ErrorEvent
|
||||
|
||||
#### Scenario: redacted_thinking 块丢弃
|
||||
|
||||
- **WHEN** content_block_start 事件中 content_block.type 为 "redacted_thinking"
|
||||
- **THEN** SHALL 将 index 加入 redactedBlocks
|
||||
- **THEN** 后续该 index 的 delta 和 stop 事件 SHALL 丢弃
|
||||
|
||||
#### Scenario: 协议特有 delta 丢弃
|
||||
|
||||
- **WHEN** delta 类型为 citations_delta 或 signature_delta
|
||||
- **THEN** SHALL 丢弃,不影响 block 生命周期
|
||||
|
||||
#### Scenario: 服务端工具块丢弃
|
||||
|
||||
- **WHEN** content_block_start 事件中类型为 server_tool_use / web_search_tool_result 等
|
||||
- **THEN** SHALL 丢弃整个 block
|
||||
|
||||
### Requirement: Anthropic 流式编码器
|
||||
|
||||
系统 SHALL 实现 AnthropicStreamEncoder,将 CanonicalStreamEvent 编码为 Anthropic 命名 SSE 事件。
|
||||
|
||||
#### Scenario: 直接映射,无缓冲
|
||||
|
||||
- **WHEN** 收到任意 CanonicalStreamEvent
|
||||
- **THEN** SHALL 直接编码为对应的 Anthropic SSE 事件
|
||||
- **THEN** SHALL NOT 缓冲等待(与 OpenAI 编码器不同)
|
||||
|
||||
#### Scenario: SSE 格式
|
||||
|
||||
- **WHEN** 编码输出
|
||||
- **THEN** SHALL 使用 `event: <type>\ndata: <json>\n\n` 格式
|
||||
|
||||
#### Scenario: Delta 类型编码
|
||||
|
||||
- **WHEN** delta.type == "text_delta"
|
||||
- **THEN** SHALL 编码为 Anthropic text_delta
|
||||
- **WHEN** delta.type == "input_json_delta"
|
||||
- **THEN** SHALL 编码为 Anthropic input_json_delta
|
||||
- **WHEN** delta.type == "thinking_delta"
|
||||
- **THEN** SHALL 编码为 Anthropic thinking_delta
|
||||
|
||||
### Requirement: Anthropic 错误编码
|
||||
|
||||
系统 SHALL 实现 Anthropic 协议的错误编码。
|
||||
|
||||
#### Scenario: 错误响应格式
|
||||
|
||||
- **WHEN** 调用 encodeError(conversionError)
|
||||
- **THEN** SHALL 返回 `{type: "error", error: {type: <error_code>, message: <message>}}`
|
||||
|
||||
### Requirement: Anthropic 扩展层接口编解码
|
||||
|
||||
系统 SHALL 实现 Anthropic 协议的扩展层接口编解码(仅 Models)。
|
||||
|
||||
#### Scenario: /models 列表接口
|
||||
|
||||
- **WHEN** 解码 Anthropic models 响应
|
||||
- **THEN** data[].display_name SHALL 映射为 models[].name
|
||||
- **THEN** data[].created_at(RFC 3339)SHALL 转换为 models[].created(Unix 时间戳)
|
||||
- **WHEN** 编码 CanonicalModelList 为 Anthropic 格式
|
||||
- **THEN** SHALL 输出包含 has_more、first_id、last_id 的结构
|
||||
- **THEN** models[].created(Unix 时间戳)SHALL 转换为 RFC 3339 字符串
|
||||
|
||||
#### Scenario: /models 详情接口
|
||||
|
||||
- **WHEN** 解码/编码 model 详情
|
||||
- **THEN** SHALL 处理 display_name ↔ name 和 RFC 3339 ↔ Unix 时间戳的转换
|
||||
|
||||
#### Scenario: EMBEDDINGS 和 RERANK 不支持
|
||||
|
||||
- **WHEN** interfaceType 为 EMBEDDINGS 或 RERANK
|
||||
- **THEN** supportsInterface SHALL 返回 false
|
||||
- **THEN** 引擎 SHALL 走透传或返回空响应
|
||||
@@ -0,0 +1,268 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 实现 OpenAI ProtocolAdapter
|
||||
|
||||
系统 SHALL 实现 OpenAI 协议的完整 ProtocolAdapter,对照 `docs/conversion_openai.md`。
|
||||
|
||||
- `protocolName()` SHALL 返回 `"openai"`
|
||||
- `supportsPassthrough()` SHALL 返回 true
|
||||
- `buildHeaders(provider)` SHALL 构建 `Authorization: Bearer <api_key>` 和 `Content-Type: application/json`
|
||||
- `buildUrl(nativePath, interfaceType)` SHALL 按接口类型映射 URL 路径
|
||||
- `supportsInterface()` SHALL 对 CHAT、MODELS、MODEL_INFO、EMBEDDINGS、RERANK 返回 true
|
||||
|
||||
#### Scenario: 认证 Header 构建
|
||||
|
||||
- **WHEN** 调用 buildHeaders(provider)
|
||||
- **THEN** SHALL 设置 `Authorization: Bearer <provider.api_key>`
|
||||
- **THEN** SHALL 设置 `Content-Type: application/json`
|
||||
- **WHEN** provider.adapter_config 包含 organization
|
||||
- **THEN** SHALL 设置 `OpenAI-Organization` Header
|
||||
|
||||
#### Scenario: URL 映射
|
||||
|
||||
- **WHEN** interfaceType == CHAT
|
||||
- **THEN** SHALL 映射为 `/v1/chat/completions`
|
||||
- **WHEN** interfaceType == MODELS
|
||||
- **THEN** SHALL 映射为 `/v1/models`
|
||||
- **WHEN** interfaceType == EMBEDDINGS
|
||||
- **THEN** SHALL 映射为 `/v1/embeddings`
|
||||
|
||||
### Requirement: OpenAI 请求解码(OpenAI → Canonical)
|
||||
|
||||
系统 SHALL 实现完整的 OpenAI ChatCompletionRequest 到 CanonicalRequest 的解码。
|
||||
|
||||
#### Scenario: System/Developer 消息提取
|
||||
|
||||
- **WHEN** OpenAI messages 中包含 role="system" 或 role="developer" 的消息
|
||||
- **THEN** SHALL 提取为 canonical.system(String)
|
||||
- **THEN** 多条 system/developer 消息 SHALL 合并以 `\n\n` 分隔
|
||||
- **THEN** SHALL 从 messages 数组中移除这些消息
|
||||
|
||||
#### Scenario: Assistant 消息中的 tool_calls 解码
|
||||
|
||||
- **WHEN** OpenAI assistant 消息包含 tool_calls
|
||||
- **THEN** SHALL 将每个 tool_call 解码为 ContentBlock{type: "tool_use", id, name, input}
|
||||
- **THEN** function.arguments(JSON 字符串)SHALL 解析为原始 JSON 对象
|
||||
|
||||
#### Scenario: Tool 消息解码
|
||||
|
||||
- **WHEN** OpenAI 消息 role="tool"
|
||||
- **THEN** SHALL 解码为 CanonicalMessage{role: "tool", content: [ToolResultBlock{tool_use_id, content}]}
|
||||
- **THEN** tool_call_id SHALL 映射为 tool_use_id
|
||||
|
||||
#### Scenario: 参数映射
|
||||
|
||||
- **WHEN** 解码 OpenAI 请求参数
|
||||
- **THEN** max_completion_tokens(优先)或 max_tokens SHALL 映射为 parameters.max_tokens
|
||||
- **THEN** stop(String 或 Array)SHALL 规范化为 parameters.stop_sequences(Array)
|
||||
- **THEN** temperature/top_p/frequency_penalty/presence_penalty SHALL 直接映射
|
||||
|
||||
#### Scenario: 公共字段映射
|
||||
|
||||
- **WHEN** 解码 OpenAI 公共字段
|
||||
- **THEN** user SHALL 映射为 user_id
|
||||
- **THEN** response_format SHALL 映射为 output_format
|
||||
- **THEN** parallel_tool_calls SHALL 映射为 parallel_tool_use
|
||||
- **THEN** reasoning_effort SHALL 映射为 thinking 配置("none" → disabled, 其他 → enabled+effort)
|
||||
|
||||
#### Scenario: 废弃字段兼容
|
||||
|
||||
- **WHEN** OpenAI 请求包含 functions 或 function_call 字段
|
||||
- **THEN** SHALL 转换为对应的 tools/tool_choice 格式
|
||||
- **THEN** 仅在 tools/tool_choice 未设置时使用废弃字段
|
||||
|
||||
### Requirement: OpenAI 请求编码(Canonical → OpenAI)
|
||||
|
||||
系统 SHALL 实现完整的 CanonicalRequest 到 OpenAI ChatCompletionRequest 的编码。
|
||||
|
||||
#### Scenario: 模型名称覆盖
|
||||
|
||||
- **WHEN** 编码请求
|
||||
- **THEN** SHALL 使用 provider.model_name 覆盖 canonical.model
|
||||
|
||||
#### Scenario: System 消息注入
|
||||
|
||||
- **WHEN** canonical.system 不为空
|
||||
- **THEN** SHALL 编码为 messages 数组头部的 role="system" 消息
|
||||
|
||||
#### Scenario: Assistant 消息中 tool_calls 编码
|
||||
|
||||
- **WHEN** CanonicalMessage{role: "assistant"} 包含 tool_use 类型 ContentBlock
|
||||
- **THEN** SHALL 提取到 message.tool_calls 数组({id, type: "function", function: {name, arguments}})
|
||||
- **THEN** arguments SHALL 序列化为 JSON 字符串
|
||||
|
||||
#### Scenario: 角色交替合并
|
||||
|
||||
- **WHEN** Canonical 消息序列中存在连续同角色消息
|
||||
- **THEN** SHALL 合并为单条 OpenAI 消息
|
||||
- **THEN** 文本内容 SHALL 合并连接
|
||||
|
||||
#### Scenario: 参数编码
|
||||
|
||||
- **WHEN** 编码 CanonicalRequest 参数
|
||||
- **THEN** parameters.max_tokens SHALL 映射为 max_completion_tokens
|
||||
- **THEN** thinking.type=="disabled" SHALL 映射为 reasoning_effort="none"
|
||||
- **THEN** thinking.effort SHALL 直接映射为 reasoning_effort
|
||||
|
||||
### Requirement: OpenAI 响应解码(OpenAI → Canonical)
|
||||
|
||||
系统 SHALL 实现 OpenAI ChatCompletionResponse 到 CanonicalResponse 的解码。
|
||||
|
||||
#### Scenario: 内容块解码
|
||||
|
||||
- **WHEN** OpenAI response.choice[0].message 包含 content
|
||||
- **THEN** SHALL 解码为 TextBlock
|
||||
- **WHEN** 包含 tool_calls
|
||||
- **THEN** SHALL 解码为 ToolUseBlock 数组
|
||||
- **WHEN** 包含 reasoning_content(非标准,兼容提供商)
|
||||
- **THEN** SHALL 解码为 ThinkingBlock
|
||||
|
||||
#### Scenario: 停止原因映射
|
||||
|
||||
- **WHEN** 解码 finish_reason
|
||||
- **THEN** "stop" SHALL 映射为 "end_turn"
|
||||
- **THEN** "length" SHALL 映射为 "max_tokens"
|
||||
- **THEN** "tool_calls" SHALL 映射为 "tool_use"
|
||||
- **THEN** "content_filter" SHALL 映射为 "content_filter"
|
||||
|
||||
#### Scenario: Usage 映射
|
||||
|
||||
- **WHEN** 解码 OpenAI usage
|
||||
- **THEN** prompt_tokens SHALL 映射为 input_tokens
|
||||
- **THEN** completion_tokens SHALL 映射为 output_tokens
|
||||
- **THEN** prompt_tokens_details.cached_tokens SHALL 映射为 cache_read_tokens
|
||||
|
||||
### Requirement: OpenAI 响应编码(Canonical → OpenAI)
|
||||
|
||||
系统 SHALL 实现 CanonicalResponse 到 OpenAI ChatCompletionResponse 的编码。
|
||||
|
||||
#### Scenario: ThinkingBlock 编码
|
||||
|
||||
- **WHEN** CanonicalResponse 包含 ThinkingBlock
|
||||
- **THEN** SHALL 编码为 message.reasoning_content(非标准字段,兼容提供商使用)
|
||||
|
||||
#### Scenario: 降级处理
|
||||
|
||||
- **WHEN** canonical.stop_reason 为 "stop_sequence" 或 "refusal"
|
||||
- **THEN** SHALL 映射为 finish_reason "stop"
|
||||
- **WHEN** canonical.stop_reason 为 "pause_turn"
|
||||
- **THEN** SHALL 映射为 finish_reason "stop"(降级)
|
||||
|
||||
### Requirement: OpenAI 流式解码器
|
||||
|
||||
系统 SHALL 实现 OpenAIStreamDecoder,将 OpenAI SSE delta chunk 转换为 CanonicalStreamEvent。
|
||||
|
||||
Decoder SHALL 维护状态机:
|
||||
- messageStarted: 是否已发送 MessageStartEvent
|
||||
- openBlocks: 当前打开的 block index 集合
|
||||
- toolCallIdMap/toolCallNameMap/toolCallArguments: 工具调用索引映射和参数累积
|
||||
- textBlockStarted/thinkingBlockStarted: 文本/思考 block 生命周期追踪
|
||||
- utf8Remainder: UTF-8 跨 chunk 安全缓冲
|
||||
|
||||
#### Scenario: 首个 chunk 触发 MessageStartEvent
|
||||
|
||||
- **WHEN** 收到第一个有效 chunk
|
||||
- **THEN** SHALL 发出 MessageStartEvent,包含 id 和 model
|
||||
|
||||
#### Scenario: delta.content 触发 text block 事件
|
||||
|
||||
- **WHEN** 收到 delta.content 首次出现
|
||||
- **THEN** SHALL 发出 ContentBlockStartEvent(text) + ContentBlockDeltaEvent(text_delta)
|
||||
- **WHEN** 收到 delta.content 后续出现
|
||||
- **THEN** SHALL 发出 ContentBlockDeltaEvent(text_delta)
|
||||
|
||||
#### Scenario: delta.tool_calls 触发 tool_use block 事件
|
||||
|
||||
- **WHEN** delta.tool_calls[i] 首次出现(含 id)
|
||||
- **THEN** SHALL 发出 ContentBlockStartEvent(tool_use)
|
||||
- **WHEN** delta.tool_calls[i].function.arguments 增量到达
|
||||
- **THEN** SHALL 发出 ContentBlockDeltaEvent(input_json_delta)
|
||||
|
||||
#### Scenario: delta.reasoning_content 触发 thinking block 事件
|
||||
|
||||
- **WHEN** delta.reasoning_content 出现(非标准字段)
|
||||
- **THEN** SHALL 发出 ContentBlockStartEvent(thinking) + ContentBlockDeltaEvent(thinking_delta)
|
||||
|
||||
#### Scenario: finish_reason 触发关闭事件
|
||||
|
||||
- **WHEN** finish_reason 非空
|
||||
- **THEN** SHALL 为所有 open blocks 发出 ContentBlockStopEvent
|
||||
- **THEN** SHALL 发出 MessageDeltaEvent(含 stop_reason 映射)
|
||||
- **THEN** SHALL 发出 MessageStopEvent
|
||||
|
||||
#### Scenario: usage chunk 处理
|
||||
|
||||
- **WHEN** 收到 choices 为空但含 usage 的 chunk
|
||||
- **THEN** SHALL 发出 MessageDeltaEvent(仅含 usage)
|
||||
|
||||
#### Scenario: [DONE] 信号处理
|
||||
|
||||
- **WHEN** 收到 `data: [DONE]`
|
||||
- **THEN** SHALL 触发 flush() 关闭所有 open blocks
|
||||
|
||||
#### Scenario: UTF-8 跨 chunk 安全
|
||||
|
||||
- **WHEN** chunk 边界截断了 UTF-8 多字节序列
|
||||
- **THEN** SHALL 使用 utf8Remainder 缓冲不完整字节
|
||||
- **THEN** 下一个 chunk 到达时 SHALL 拼接后重新解析
|
||||
|
||||
### Requirement: OpenAI 流式编码器
|
||||
|
||||
系统 SHALL 实现 OpenAIStreamEncoder,将 CanonicalStreamEvent 编码为 OpenAI SSE chunk。
|
||||
|
||||
Encoder SHALL 维护状态:
|
||||
- bufferedStart: 缓冲的 ContentBlockStartEvent
|
||||
- toolCallIndexMap: tool_use_id → OpenAI tool_calls 数组索引映射
|
||||
|
||||
#### Scenario: ContentBlockStart 缓冲策略
|
||||
|
||||
- **WHEN** 收到 ContentBlockStartEvent
|
||||
- **THEN** SHALL NOT 立即输出,缓冲等待首次 ContentBlockDeltaEvent
|
||||
|
||||
#### Scenario: ContentBlockDelta 合并输出
|
||||
|
||||
- **WHEN** 收到 ContentBlockDeltaEvent 且有缓冲的 StartEvent
|
||||
- **THEN** SHALL 合并 Start 信息(如 tool id/name)与 delta 数据一起输出
|
||||
- **WHEN** 无缓冲 StartEvent
|
||||
- **THEN** SHALL 仅输出 delta 数据
|
||||
|
||||
#### Scenario: MessageStopEvent 输出 [DONE]
|
||||
|
||||
- **WHEN** 收到 MessageStopEvent
|
||||
- **THEN** SHALL 输出 `data: [DONE]`
|
||||
|
||||
#### Scenario: PingEvent 和 ErrorEvent 处理
|
||||
|
||||
- **WHEN** 收到 PingEvent 或 ErrorEvent
|
||||
- **THEN** SHALL 不输出(OpenAI 无流式错误/心跳事件)
|
||||
|
||||
### Requirement: OpenAI 错误编码
|
||||
|
||||
系统 SHALL 实现 OpenAI 协议的错误编码。
|
||||
|
||||
#### Scenario: 错误响应格式
|
||||
|
||||
- **WHEN** 调用 encodeError(conversionError)
|
||||
- **THEN** SHALL 返回 `{error: {message, type, param: null, code}}`
|
||||
- **THEN** ErrorCode SHALL 映射为 OpenAI 错误类型(如 INVALID_INPUT → "invalid_request_error")
|
||||
|
||||
### Requirement: OpenAI 扩展层接口编解码
|
||||
|
||||
系统 SHALL 实现 OpenAI 协议的扩展层接口编解码。
|
||||
|
||||
#### Scenario: /models 列表接口
|
||||
|
||||
- **WHEN** 解码 OpenAI models 响应
|
||||
- **THEN** SHALL 映射为 CanonicalModelList(data[].id → models[].id, created, owned_by)
|
||||
- **WHEN** 编码 CanonicalModelList 为 OpenAI 格式
|
||||
- **THEN** SHALL 输出 `{object: "list", data: [...]}`
|
||||
|
||||
#### Scenario: /embeddings 接口
|
||||
|
||||
- **WHEN** 解码/编码 embedding 请求和响应
|
||||
- **THEN** SHALL 使用 CanonicalEmbeddingRequest/Response 做字段映射
|
||||
|
||||
#### Scenario: /rerank 接口
|
||||
|
||||
- **WHEN** 解码/编码 rerank 请求和响应
|
||||
- **THEN** SHALL 使用 CanonicalRerankRequest/Response 做字段映射
|
||||
@@ -0,0 +1,73 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 创建供应商配置
|
||||
|
||||
网关 SHALL 允许通过管理 API 创建新的供应商配置。
|
||||
|
||||
#### Scenario: 使用有效数据创建供应商
|
||||
|
||||
- **WHEN** 向 `/api/providers` 发送 POST 请求,携带有效的供应商数据(id, name, api_key, base_url, protocol)
|
||||
- **THEN** 网关 SHALL 在数据库中创建新的供应商记录
|
||||
- **THEN** 网关 SHALL 返回创建的供应商,状态码为 201
|
||||
- **THEN** 供应商 SHALL 默认启用
|
||||
- **THEN** protocol 字段 SHALL 默认为 "openai"
|
||||
|
||||
#### Scenario: 使用重复 ID 创建供应商
|
||||
|
||||
- **WHEN** 向 `/api/providers` 发送 POST 请求,携带已存在的 ID
|
||||
- **THEN** 网关 SHALL 返回错误,状态码为 409 (Conflict)
|
||||
|
||||
#### Scenario: 创建供应商时缺少必需字段
|
||||
|
||||
- **WHEN** 向 `/api/providers` 发送 POST 请求,缺少必需字段(id, name, api_key 或 base_url)
|
||||
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
|
||||
- **THEN** 错误 SHALL 指示缺少哪些字段
|
||||
|
||||
### Requirement: 列出所有供应商
|
||||
|
||||
网关 SHALL 允许获取所有供应商配置。
|
||||
|
||||
#### Scenario: 成功列出供应商
|
||||
|
||||
- **WHEN** 向 `/api/providers` 发送 GET 请求
|
||||
- **THEN** 网关 SHALL 返回所有供应商的列表
|
||||
- **THEN** 每个供应商 SHALL 包含 id, name, api_key(已掩码), base_url, protocol, enabled, created_at, updated_at
|
||||
- **THEN** api_key SHALL 被掩码(仅显示最后 4 个字符)
|
||||
|
||||
### Requirement: 获取特定供应商
|
||||
|
||||
网关 SHALL 允许通过 ID 获取特定供应商。
|
||||
|
||||
#### Scenario: 获取存在的供应商
|
||||
|
||||
- **WHEN** 向 `/api/providers/:id` 发送 GET 请求,携带有效的供应商 ID
|
||||
- **THEN** 网关 SHALL 返回供应商详情
|
||||
- **THEN** SHALL 包含 protocol 字段
|
||||
- **THEN** api_key SHALL 被掩码
|
||||
|
||||
#### Scenario: 获取不存在的供应商
|
||||
|
||||
- **WHEN** 向 `/api/providers/:id` 发送 GET 请求,携带不存在的 ID
|
||||
- **THEN** 网关 SHALL 返回错误,状态码为 404 (Not Found)
|
||||
|
||||
### Requirement: 更新供应商配置
|
||||
|
||||
网关 SHALL 允许更新现有供应商配置。
|
||||
|
||||
#### Scenario: 使用有效数据更新供应商
|
||||
|
||||
- **WHEN** 向 `/api/providers/:id` 发送 PUT 请求,携带有效的供应商数据
|
||||
- **THEN** 网关 SHALL 更新数据库中的供应商记录
|
||||
- **THEN** 网关 SHALL 返回更新后的供应商
|
||||
- **THEN** 更新 SHALL 支持修改 protocol 字段
|
||||
|
||||
### Requirement: 删除供应商配置
|
||||
|
||||
网关 SHALL 允许删除供应商配置。
|
||||
|
||||
#### Scenario: 删除存在的供应商
|
||||
|
||||
- **WHEN** 向 `/api/providers/:id` 发送 DELETE 请求,携带有效的供应商 ID
|
||||
- **THEN** 网关 SHALL 删除供应商记录
|
||||
- **THEN** 网关 SHALL 删除所有关联的模型(CASCADE)
|
||||
- **THEN** 网关 SHALL 返回状态码 204 (No Content)
|
||||
@@ -0,0 +1,64 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 验证 OpenAI 请求
|
||||
|
||||
系统 SHALL 验证 OpenAI ChatCompletionRequest,验证逻辑位于 ProtocolAdapter 的 decodeRequest 内。
|
||||
|
||||
#### Scenario: 必需字段验证
|
||||
|
||||
- **WHEN** OpenAI Adapter 的 decodeRequest 解析请求
|
||||
- **THEN** SHALL 验证 model 字段不为空
|
||||
- **THEN** SHALL 验证 messages 字段不为空且至少有一条消息
|
||||
- **THEN** 验证失败 SHALL 返回 INVALID_INPUT 类型的 ConversionError
|
||||
|
||||
#### Scenario: 参数范围验证
|
||||
|
||||
- **WHEN** OpenAI Adapter 的 decodeRequest 解析参数
|
||||
- **THEN** SHALL 验证 temperature 范围在 [0, 2]
|
||||
- **THEN** SHALL 验证 max_tokens 大于 0
|
||||
- **THEN** SHALL 验证 top_p 范围在 (0, 1]
|
||||
|
||||
#### Scenario: 消息内容验证
|
||||
|
||||
- **WHEN** 验证 messages 字段
|
||||
- **THEN** SHALL 验证每条消息的 role 有效(system、developer、user、assistant、tool)
|
||||
- **THEN** SHALL 验证 content 不为空
|
||||
|
||||
### Requirement: 验证 Anthropic 请求
|
||||
|
||||
系统 SHALL 验证 Anthropic MessagesRequest,验证逻辑位于 ProtocolAdapter 的 decodeRequest 内。
|
||||
|
||||
#### Scenario: 必需字段验证
|
||||
|
||||
- **WHEN** Anthropic Adapter 的 decodeRequest 解析请求
|
||||
- **THEN** SHALL 验证 model 字段不为空
|
||||
- **THEN** SHALL 验证 messages 字段不为空且至少有一条消息
|
||||
- **THEN** SHALL 验证 max_tokens 大于 0(或使用默认值)
|
||||
|
||||
#### Scenario: 参数范围验证
|
||||
|
||||
- **WHEN** Anthropic Adapter 的 decodeRequest 解析参数
|
||||
- **THEN** SHALL 验证 temperature 范围在 [0, 1]
|
||||
- **THEN** SHALL 验证 top_p 范围在 (0, 1]
|
||||
|
||||
#### Scenario: 消息内容验证
|
||||
|
||||
- **WHEN** 验证 messages 字段
|
||||
- **THEN** SHALL 验证每条消息的 role 有效(user、assistant)
|
||||
- **THEN** SHALL 验证 content 数组不为空
|
||||
|
||||
### Requirement: 返回友好的验证错误
|
||||
|
||||
系统 SHALL 返回友好的验证错误响应。
|
||||
|
||||
#### Scenario: 转换错误格式
|
||||
|
||||
- **WHEN** decodeRequest 验证失败返回 ConversionError
|
||||
- **THEN** ProxyHandler SHALL 使用 clientAdapter.encodeError 编码错误响应
|
||||
- **THEN** 错误 SHALL 使用客户端协议的格式
|
||||
|
||||
#### Scenario: 多字段错误
|
||||
|
||||
- **WHEN** 多个字段验证失败
|
||||
- **THEN** ConversionError.details SHALL 包含所有验证错误
|
||||
- **THEN** 错误响应 SHALL 包含完整的验证错误信息
|
||||
@@ -0,0 +1,102 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 实现统一代理 Handler
|
||||
|
||||
系统 SHALL 实现统一的 ProxyHandler,替代现有的 OpenAIHandler 和 AnthropicHandler。
|
||||
|
||||
ProxyHandler SHALL 依赖 ConversionEngine、ProviderClient、RoutingService、StatsService。
|
||||
|
||||
#### Scenario: 从 URL 提取客户端协议
|
||||
|
||||
- **WHEN** 收到 `/{protocol}/v1/{path}` 格式的请求
|
||||
- **THEN** SHALL 从 URL 第一段提取 protocol 作为 clientProtocol
|
||||
- **THEN** SHALL 剥离前缀得到 nativePath
|
||||
|
||||
#### Scenario: 协议前缀必须是已注册协议
|
||||
|
||||
- **WHEN** 收到的 URL 前缀不是已注册的协议名称
|
||||
- **THEN** SHALL 返回 404 错误
|
||||
|
||||
#### Scenario: 接口类型识别
|
||||
|
||||
- **WHEN** 提取 nativePath 后
|
||||
- **THEN** SHALL 通过 ConversionEngine.convertHttpRequest 内部调用 clientAdapter.detectInterfaceType(nativePath) 识别接口类型
|
||||
- **THEN** 未知路径 SHALL 使用透传模式
|
||||
|
||||
### Requirement: 非流式请求处理流程
|
||||
|
||||
ProxyHandler SHALL 按以下流程处理非流式请求。
|
||||
|
||||
#### Scenario: 完整转换流程
|
||||
|
||||
- **WHEN** 收到非流式请求
|
||||
- **THEN** SHALL 解析请求体为 JSON
|
||||
- **THEN** SHALL 调用 RoutingService.Route(modelName) 获取路由结果
|
||||
- **THEN** SHALL 从路由结果的 Provider.Protocol 获取 providerProtocol
|
||||
- **THEN** SHALL 构建 TargetProvider(base_url, api_key, model_name, adapter_config)
|
||||
- **THEN** SHALL 调用 engine.convertHttpRequest(body, clientProtocol, providerProtocol, provider)
|
||||
- **THEN** SHALL 调用 providerClient.Send(ctx, requestSpec)
|
||||
- **THEN** SHALL 调用 engine.convertHttpResponse(response, clientProtocol, providerProtocol, interfaceType)
|
||||
- **THEN** SHALL 将转换后的响应返回给客户端
|
||||
|
||||
#### Scenario: 路由失败处理
|
||||
|
||||
- **WHEN** RoutingService.Route 返回错误
|
||||
- **THEN** SHALL 使用 clientProtocol 对应的 Adapter.encodeError 编码错误响应
|
||||
- **THEN** SHALL 返回适当的 HTTP 状态码
|
||||
|
||||
#### Scenario: 上游请求失败处理
|
||||
|
||||
- **WHEN** ProviderClient.Send 返回错误
|
||||
- **THEN** SHALL 使用 clientProtocol 对应的 Adapter.encodeError 编码错误响应
|
||||
- **THEN** SHALL 包装原始错误信息
|
||||
|
||||
### Requirement: 流式请求处理流程
|
||||
|
||||
ProxyHandler SHALL 按以下流程处理流式请求。
|
||||
|
||||
#### Scenario: 流式转换流程
|
||||
|
||||
- **WHEN** 请求中 stream=true 或接口类型为 CHAT 且请求体含 stream:true
|
||||
- **THEN** SHALL 执行与非流式相同的前置处理(路由、构建 TargetProvider)
|
||||
- **THEN** SHALL 调用 engine.convertHttpRequest 获取 HTTPRequestSpec
|
||||
- **THEN** SHALL 调用 providerClient.SendStream(ctx, requestSpec) 获取流
|
||||
- **THEN** SHALL 调用 engine.createStreamConverter(clientProtocol, providerProtocol, provider)
|
||||
- **THEN** SHALL 设置响应 Header `Content-Type: text/event-stream`
|
||||
- **THEN** SHALL 对每个流 chunk 调用 converter.processChunk 并写入响应
|
||||
- **THEN** SHALL 在流结束时调用 converter.flush() 刷新缓冲
|
||||
|
||||
#### Scenario: 同协议透传流式
|
||||
|
||||
- **WHEN** clientProtocol == providerProtocol
|
||||
- **THEN** SHALL 直接将上游 SSE 字节流写入响应
|
||||
- **THEN** SHALL NOT 做任何解析或转换
|
||||
|
||||
#### Scenario: 流式错误处理
|
||||
|
||||
- **WHEN** 流过程中发生错误
|
||||
- **THEN** SHALL 记录错误日志
|
||||
- **THEN** SHALL 关闭响应流
|
||||
|
||||
### Requirement: 统计记录
|
||||
|
||||
ProxyHandler SHALL 记录请求统计。
|
||||
|
||||
#### Scenario: 异步记录统计
|
||||
|
||||
- **WHEN** 请求处理完成(成功或失败)
|
||||
- **THEN** SHALL 异步调用 StatsService.Record
|
||||
- **THEN** SHALL NOT 阻塞响应返回
|
||||
|
||||
### Requirement: GET 请求透传
|
||||
|
||||
ProxyHandler SHALL 支持 GET 请求的扩展层接口代理。
|
||||
|
||||
#### Scenario: Models 接口代理
|
||||
|
||||
- **WHEN** 收到 GET /{protocol}/v1/models 请求
|
||||
- **THEN** SHALL 执行路由和协议识别
|
||||
- **THEN** SHALL 调用 engine.convertHttpRequest(GET 请求 body 为空)
|
||||
- **THEN** SHALL 调用 providerClient.Send 发送请求
|
||||
- **THEN** SHALL 调用 engine.convertHttpResponse 转换响应格式
|
||||
- **THEN** SHALL 返回转换后的响应
|
||||
49
openspec/changes/refactor-conversion-engine/tasks.md
Normal file
49
openspec/changes/refactor-conversion-engine/tasks.md
Normal file
@@ -0,0 +1,49 @@
|
||||
## 1. 基础类型层 — Canonical Model 和核心类型定义
|
||||
|
||||
- [ ] 1.1 创建 `internal/conversion/errors.go`:定义 ConversionError 结构体(Code, Message, ClientProtocol, ProviderProtocol, InterfaceType, Details, Cause)和 ErrorCode 枚举(INVALID_INPUT, MISSING_REQUIRED_FIELD, INCOMPATIBLE_FEATURE, FIELD_MAPPING_FAILURE, TOOL_CALL_PARSE_ERROR, JSON_PARSE_ERROR, STREAM_STATE_ERROR, UTF8_DECODE_ERROR, PROTOCOL_CONSTRAINT_VIOLATION, ENCODING_FAILURE, INTERFACE_NOT_SUPPORTED),实现 error 接口
|
||||
- [ ] 1.2 创建 `internal/conversion/interface.go`:定义 InterfaceType 枚举(CHAT, MODELS, MODEL_INFO, EMBEDDINGS, RERANK)
|
||||
- [ ] 1.3 创建 `internal/conversion/provider.go`:定义 TargetProvider 结构体(BaseURL, APIKey, ModelName, AdapterConfig map[string]any);编写测试
|
||||
- [ ] 1.4 创建 `internal/conversion/canonical/types.go`:定义 CanonicalRequest(model, system, messages, tools, tool_choice, parameters, thinking, stream, user_id, output_format, parallel_tool_use)、CanonicalMessage(role 枚举: system/user/assistant/tool, content []ContentBlock)、ContentBlock(使用 type 字段的 discriminated union:text/tool_use/tool_result/thinking,ToolInput 使用 json.RawMessage)、CanonicalTool(name, description, input_schema)、ToolChoice 联合体(auto/none/any/tool+name)、RequestParameters(max_tokens, temperature, top_p, top_k, frequency_penalty, presence_penalty, stop_sequences)、ThinkingConfig(type: enabled/disabled/adaptive, budget_tokens, effort)、OutputFormat(json_object/json_schema+schema/text)、CanonicalResponse(id, model, content, stop_reason 枚举, usage)、CanonicalUsage(input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, reasoning_tokens)、SystemBlock(text);编写构造和序列化测试
|
||||
- [ ] 1.5 创建 `internal/conversion/canonical/stream.go`:定义 CanonicalStreamEvent 联合体(message_start, content_block_start, content_block_delta, content_block_stop, message_delta, message_stop, error, ping)及各事件的具体结构(MessageStartEvent 含 message{id,model,usage}、ContentBlockStartEvent 含 index 和 content_block、ContentBlockDeltaEvent 含 index 和 delta、ContentBlockStopEvent 含 index、MessageDeltaEvent 含 delta{stop_reason} 和 usage、MessageStopEvent、ErrorEvent、PingEvent),delta 联合体(text_delta, input_json_delta, thinking_delta),content_block 联合体(text, tool_use, thinking);编写测试
|
||||
- [ ] 1.6 创建 `internal/conversion/canonical/extended.go`:定义扩展层 Canonical Models(CanonicalModelList, CanonicalModel, CanonicalModelInfo, CanonicalEmbeddingRequest, CanonicalEmbeddingResponse, CanonicalRerankRequest, CanonicalRerankResponse);编写测试
|
||||
|
||||
## 2. 接口定义层 — Adapter、Stream、Middleware 接口
|
||||
|
||||
- [ ] 2.1 创建 `internal/conversion/adapter.go`:定义 ProtocolAdapter 接口(protocolName, protocolVersion, supportsPassthrough, detectInterfaceType, buildUrl, buildHeaders, supportsInterface, decodeRequest, encodeRequest, decodeResponse, encodeResponse, createStreamDecoder, createStreamEncoder, encodeError, 扩展层编解码方法:decodeModelsResponse/encodeModelsResponse/decodeModelInfoResponse/encodeModelInfoResponse/decodeEmbeddingRequest/encodeEmbeddingRequest/decodeEmbeddingResponse/encodeEmbeddingResponse/decodeRerankRequest/encodeRerankRequest/decodeRerankResponse/encodeRerankResponse),定义 AdapterRegistry 接口(register, get, listProtocols)和 memoryRegistry 实现(sync.RWMutex 保护的 map);编写 Registry 注册/查询/重复注册测试
|
||||
- [ ] 2.2 创建 `internal/conversion/stream.go`:定义 StreamDecoder 接口(processChunk(rawChunk []byte) []CanonicalStreamEvent, flush() []CanonicalStreamEvent)、StreamEncoder 接口(encodeEvent(event CanonicalStreamEvent) [][]byte, flush() [][]byte)、StreamConverter 接口(processChunk(rawChunk []byte) [][]byte, flush() [][]byte)、PassthroughStreamConverter 实现(直接传递原始字节)、CanonicalStreamConverter 实现(组合 StreamDecoder + MiddlewareChain + StreamEncoder,processChunk 内部调用 decoder → middleware → encoder 管道);编写 PassthroughStreamConverter 测试
|
||||
- [ ] 2.3 创建 `internal/conversion/middleware.go`:定义 ConversionMiddleware 接口(intercept(canonical, clientProtocol, providerProtocol, context) (CanonicalRequest, error) 和可选的 interceptStreamEvent(event, clientProtocol, providerProtocol, context) (CanonicalStreamEvent, error))、ConversionContext 结构体(conversionId, interfaceType, timestamp, metadata)、MiddlewareChain 结构体(按注册顺序链式执行,任一返回错误则中断后续);编写链式执行和中断测试
|
||||
|
||||
## 3. 引擎层 — ConversionEngine 门面
|
||||
|
||||
- [ ] 3.1 创建 `internal/conversion/engine.go`:定义 HTTPRequestSpec(URL, Method string, Headers map[string]string, Body []byte)、HTTPResponseSpec(StatusCode int, Headers map[string]string, Body []byte)、ConversionEngine struct(registry, middlewareChain);实现 registerAdapter、use、isPassthrough、convertHttpRequest(接口识别 → 透传判断 → clientAdapter.decode → middleware → providerAdapter.encode → providerAdapter.buildUrl + buildHeaders)、convertHttpResponse(透传判断 → providerAdapter.decodeResponse → clientAdapter.encodeResponse)、createStreamConverter(透传 → PassthroughStreamConverter,否则 → CanonicalStreamConverter)、内部 convertBody 分发(CHAT 走深度转换,扩展层走轻量映射,默认透传);编写集成测试:使用 mock adapter 测试跨协议转换、同协议透传、未知接口透传
|
||||
|
||||
## 4. OpenAI Adapter 实现
|
||||
|
||||
- [ ] 4.1 创建 `internal/conversion/openai/types.go`:从旧 `internal/protocol/openai/types.go` 迁移 OpenAI 线路格式类型,补全缺失字段(developer role, custom tools, reasoning_effort, reasoning_content, max_completion_tokens, parallel_tool_calls, response_format 的 json_schema 类型, stream_options, 废弃的 functions/function_call);编写序列化测试
|
||||
- [ ] 4.2 创建 `internal/conversion/openai/decoder.go`:实现 decodeRequest(对照 conversion_openai.md §4.1:decodeSystemPrompt 提取 system+developer 消息、decodeMessage 含 tool_calls/refusal/reasoning_content 解码、tool 消息 tool_call_id→tool_use_id、decodeTools 含 function+custom 类型、decodeToolChoice 含 required→any/allowed_tools 降级、decodeParameters 含 max_completion_tokens 优先、decodeOutputFormat、decodeThinking 含 reasoning_effort→ThinkingConfig、废弃字段 functions→tools 兼容)、decodeResponse(§5.2:content/refusal/reasoning_content/tool_calls 解码、finish_reason 映射表、usage 映射含 cached_tokens/reasoning_tokens)、扩展层 decode(decodeModelsResponse、decodeEmbeddingRequest/Response、decodeRerankRequest/Response);编写完整测试覆盖每类消息和字段映射
|
||||
- [ ] 4.3 创建 `internal/conversion/openai/encoder.go`:实现 encodeRequest(对照 conversion_openai.md §4.2:provider.model_name 覆盖、system 注入到 messages[0]、encodeMessage 含 tool_calls 编码到 message 顶层、角色交替合并、encodeTools 含 function 包装、encodeToolChoice 含 any→required、encodeParameters 含 max_completion_tokens、encodeOutputFormat、encodeThinking 含 disabled→"none")、encodeResponse(§5.3:text→content、tool_use→tool_calls、thinking→reasoning_content、finish_reason 反向映射、usage 编码含 prompt_tokens_details)、扩展层 encode(encodeModelsResponse、encodeEmbeddingRequest/Response、encodeRerankRequest/Response);编写完整测试
|
||||
- [ ] 4.4 创建 `internal/conversion/openai/adapter.go`:实现 OpenAI ProtocolAdapter(protocolName→"openai"、supportsPassthrough→true、detectInterfaceType 根据正则匹配识别 /v1/chat/completions→CHAT、/v1/models→MODELS 等、buildHeaders 含 Authorization+Content-Type+OpenAI-Organization、buildUrl 按接口类型映射、supportsInterface 对 CHAT/MODELS/MODEL_INFO/EMBEDDINGS/RERANK 返回 true、encodeError 含 ErrorCode→OpenAI 错误类型映射),组合 decoder 和 encoder 方法;编写测试覆盖所有路径模式和边界情况
|
||||
- [ ] 4.5 创建 `internal/conversion/openai/stream_decoder.go`:实现 OpenAIStreamDecoder(对照 conversion_openai.md §6.2-§6.3:processChunk 解析 SSE data 行,维护状态机 messageStarted/openBlocks/toolCallIdMap/toolCallNameMap/toolCallArguments/textBlockStarted/thinkingBlockStarted/utf8Remainder/accumulatedUsage,首个 chunk→MessageStartEvent,delta.content→text block 生命周期,delta.tool_calls→tool_use block 生命周期含索引映射和参数累积,delta.reasoning_content→thinking block(非标准),delta.refusal→text block,finish_reason→关闭所有 open blocks + MessageDeltaEvent + MessageStopEvent,usage chunk→MessageDeltaEvent,[DONE]→flush 关闭);编写测试覆盖每种 delta 类型和边界情况(空 chunk、多 tool_calls、UTF-8 截断)
|
||||
- [ ] 4.6 创建 `internal/conversion/openai/stream_encoder.go`:实现 OpenAIStreamEncoder(对照 conversion_openai.md §6.4:encodeEvent,ContentBlockStart 缓冲策略等待首次 ContentBlockDelta 合并输出,tool_use id/name 在首次 delta 时合并编码,text_delta 直接输出 data: {choices:[{delta:{content}}]},input_json_delta 含 tool_calls 数组编码,thinking_delta 含 reasoning_content 字段,MessageStartEvent→{choices:[{delta:{role:"assistant"}}]},MessageDeltaEvent→{choices:[{delta:{},finish_reason}]},MessageStopEvent→[DONE],PingEvent/ErrorEvent 丢弃,flush 输出缓冲区);编写测试
|
||||
|
||||
## 5. Anthropic Adapter 实现(与 Layer 4 并行)
|
||||
|
||||
- [ ] 5.1 创建 `internal/conversion/anthropic/types.go`:从旧 `internal/protocol/anthropic/types.go` 迁移 Anthropic 线路格式类型,补全缺失字段(thinking.type 含 adaptive、output_config.format/effort、disable_parallel_tool_use、metadata.user_id、redacted_thinking、pause_turn/refusal stop_reason、stop_details、container、cache_control);编写序列化测试
|
||||
- [ ] 5.2 创建 `internal/conversion/anthropic/decoder.go`:实现 decodeRequest(对照 conversion_anthropic.md §4.1:decodeSystem 从顶层 system 提取、decodeMessage 含 tool_result 从 user 消息拆分为独立 tool 角色消息、参数直接映射含 top_k、decodeThinking 含 enabled/disabled/adaptive 三种类型、decodeOutputFormat 仅支持 json_schema、公共字段提取含 metadata.user_id/disable_parallel_tool_use 反转/output_config.effort、协议特有字段 redacted_thinking 丢弃/cache_control 忽略)、decodeResponse(§5.2:text/tool_use/thinking 块解码、redacted_thinking 丢弃、stop_reason 映射含 pause_turn/refusal、usage 映射含 cache_read_input_tokens/cache_creation_input_tokens)、扩展层 decode(decodeModelsResponse 含 RFC3339→Unix 时间戳转换、decodeModelInfoResponse);编写完整测试覆盖角色拆分、thinking 三种类型、时间戳转换
|
||||
- [ ] 5.3 创建 `internal/conversion/anthropic/encoder.go`:实现 encodeRequest(对照 conversion_anthropic.md §4.2:provider.model_name 覆盖、system 注入为顶层字段、encodeMessages 含 tool→user 合并(优先合并到相邻 user 消息)、首消息 user 保证(自动注入空 user)、角色交替合并、encodeThinkingConfig 含 enabled/disabled/adaptive、encodeOutputFormat 含 json_object→空 schema 降级/text 丢弃、公共字段编码含 metadata.user_id/disable_parallel_tool_use 反转/output_config、参数编码含 max_tokens 必填/top_k 直接映射)、encodeResponse(§5.3:text/tool_use/thinking 块直接编码、stop_reason 映射含 content_filter→end_turn 降级、usage 编码含 cache_read_input_tokens/cache_creation_input_tokens)、扩展层 encode(encodeModelsResponse 含 Unix→RFC3339 转换和 has_more/first_id/last_id 字段、encodeModelInfoResponse);编写完整测试覆盖角色合并、首消息注入、降级处理
|
||||
- [ ] 5.4 创建 `internal/conversion/anthropic/adapter.go`:实现 Anthropic ProtocolAdapter(protocolName→"anthropic"、supportsPassthrough→true、detectInterfaceType 根据正则匹配识别 /v1/messages→CHAT、/v1/models→MODELS 等、buildHeaders 含 x-api-key + anthropic-version + anthropic-beta + Content-Type、buildUrl 按接口类型映射、supportsInterface 对 CHAT/MODELS/MODEL_INFO 返回 true 对 EMBEDDINGS/RERANK 返回 false、encodeError 返回 {type:"error",error:{type,message}});编写测试覆盖所有路径模式和边界情况
|
||||
- [ ] 5.5 创建 `internal/conversion/anthropic/stream_decoder.go`:实现 AnthropicStreamDecoder(对照 conversion_anthropic.md §6.2-§6.3:解析命名 SSE 事件 event: message_start/data: {...},1:1 映射到 CanonicalStreamEvent,维护状态 messageStarted/redactedBlocks/utf8Remainder/accumulatedUsage,redacted_thinking 检测后加入 redactedBlocks 并丢弃后续 delta/stop,citations_delta/signature_delta 直接丢弃,server_tool_use 等服务端工具块丢弃,UTF-8 跨 chunk 安全处理);编写测试覆盖所有事件类型和 redacted_thinking 丢弃
|
||||
- [ ] 5.6 创建 `internal/conversion/anthropic/stream_encoder.go`:实现 AnthropicStreamEncoder(对照 conversion_anthropic.md §6.4:直接映射无缓冲,每个 CanonicalStreamEvent 直接编码为对应的 Anthropic 命名 SSE 事件,格式 event: `<type>`\ndata: `<json>`\n\n,delta 编码 text_delta/input_json_delta/thinking_delta 直接映射);编写测试
|
||||
|
||||
## 6. 基础设施改造 — Provider、Handler、Domain
|
||||
|
||||
- [ ] 6.1 修改 `internal/domain/provider.go`:Provider 结构体新增 Protocol string 字段;修改 `internal/config/models.go`:GORM Provider 模型同步新增 Protocol 字段(gorm:"column:protocol;default:'openai'");修改 `internal/repository/` 中 toDomainProvider 和 toConfigProvider 转换函数同步 Protocol 字段;修改 `internal/handler/provider_handler.go`:CreateProvider 和 UpdateProvider 的请求结构体新增 Protocol 字段(可选,默认 "openai"),创建/更新 Provider 时赋值 Protocol 字段,List/Get 响应中包含 Protocol 字段;更新 `internal/service/service_test.go` 中所有创建测试 Provider 的地方补充 Protocol 字段;更新 `internal/handler/handler_test.go` 中 Provider CRUD 测试的请求体补充 Protocol 字段;创建数据库迁移文件 `backend/migrations/YYYYMMDDHHMMSS_add_provider_protocol.sql`:ALTER TABLE providers ADD COLUMN protocol TEXT DEFAULT 'openai'
|
||||
- [ ] 6.2 重写 `internal/provider/client.go`:定义 HTTPRequestSpec 和 HTTPResponseSpec(或引用 conversion 包的定义),简化 ProviderClient 接口为 Send(ctx, HTTPRequestSpec) → (*HTTPResponseSpec, error) 和 SendStream(ctx, HTTPRequestSpec) → (<-chan StreamEvent, error),移除所有 openai.Adapter 硬编码依赖,Send 方法直接使用 http.NewRequest + spec.URL/Headers/Body,SendStream 保留现有 readStream goroutine 逻辑但输入改为 HTTPRequestSpec;重写 `provider/client_test.go`:删除所有基于 openai.ChatCompletionRequest 的旧测试用例,基于 HTTPRequestSpec 重写成功/失败/流式测试用例,使用 httptest.Server 验证请求构建和响应解析
|
||||
- [ ] 6.3 创建 `internal/handler/proxy_handler.go`:实现 ProxyHandler struct(依赖 ConversionEngine、ProviderClient、RoutingService、StatsService),实现 HandleProxy(w, r) 方法:从 URL 提取 clientProtocol(仅支持 `/{protocol}/v1/...` 前缀路由,不支持旧路由)、解析请求体 JSON、调用 RoutingService.Route(modelName) 获取路由结果(含 Provider.Protocol 作为 providerProtocol)、构建 TargetProvider、调用 engine.convertHttpRequest、调用 providerClient.Send/SendStream、调用 engine.convertHttpResponse、设置响应 Content-Type 和状态码、流式处理设置 text/event-stream 并用 StreamConverter 逐块转换写入、错误处理使用 clientAdapter.encodeError、异步调用 StatsService.Record;编写测试使用 httptest + mock engine/client/service
|
||||
- [ ] 6.4 修改 `cmd/server/main.go`:创建 AdapterRegistry 并注册 OpenAI 和 Anthropic Adapter、创建 ConversionEngine(注入 registry)、创建 ProxyHandler(注入 engine + providerClient + routingService + statsService)、配置 Gin 路由:新增 `/{protocol}/v1/{path:*}` → ProxyHandler.HandleProxy,删除旧路由 `/v1/chat/completions` 和 `/v1/messages`,移除旧的 OpenAIHandler 和 AnthropicHandler 的路由注册,移除旧的 Adapter 创建代码
|
||||
|
||||
## 7. 清理和文档
|
||||
|
||||
- [ ] 7.1 删除旧代码:删除 `internal/protocol/openai/` 目录(types.go, adapter.go, adapter_test.go)、删除 `internal/protocol/anthropic/` 目录(types.go, converter.go, converter_test.go, stream_converter.go, stream_converter_test.go)、删除 `internal/handler/openai_handler.go` 和 `internal/handler/anthropic_handler.go`、删除 `internal/handler/handler_test.go` 中旧 OpenAI/Anthropic handler 测试用例和旧 `mockProviderClient`(基于 openai.ChatCompletionRequest 的签名)、重写 `handler_test.go` 为 ProxyHandler 测试(基于新 ProviderClient 接口和 ConversionEngine mock)、删除 `internal/protocol/` 空目录、确认所有编译通过且无残留 import
|
||||
- [ ] 7.2 更新 `README.md`:更新项目结构说明(新增 internal/conversion/、删除 internal/protocol/)、更新 API 接口说明(代理接口变更:`/{protocol}/v1/...`,移除旧路由 `/v1/chat/completions` 和 `/v1/messages`)、更新配置说明(Provider 新增 protocol 字段)
|
||||
- [ ] 7.3 端到端测试:在 `backend/tests/integration/` 中新增 `conversion_test.go`,使用 httptest mock 上游服务器验证完整请求流:OpenAI→OpenAI 同协议透传、Anthropic→Anthropic 同协议透传、OpenAI→Anthropic 跨协议非流式、Anthropic→OpenAI 跨协议非流式、4 种方向的流式转换(含 tool_calls 和 thinking)、Models 接口跨协议转换、错误响应格式验证(各协议格式)、旧路由 `/v1/chat/completions` 和 `/v1/messages` 返回 404;复用 `tests/helpers.go` 中的测试数据库和 Provider/Model 创建辅助函数
|
||||
Reference in New Issue
Block a user