-
Notifications
You must be signed in to change notification settings - Fork 6
feat(llm): expose provider request extensions #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2c3ff81
d1725af
6392a92
7dd207a
46f292e
55875a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,6 +84,9 @@ pub enum ReasoningEffort { | |
| Medium, | ||
| /// Above-default effort. | ||
| High, | ||
| /// Maximum provider-supported effort. | ||
| #[serde(rename = "xhigh")] | ||
| XHigh, | ||
| /// Explicitly disable reasoning. | ||
| None, | ||
| } | ||
|
|
@@ -96,6 +99,7 @@ impl ReasoningEffort { | |
| Self::Low => "low", | ||
| Self::Medium => "medium", | ||
| Self::High => "high", | ||
| Self::XHigh => "xhigh", | ||
| Self::None => "none", | ||
| } | ||
| } | ||
|
|
@@ -917,6 +921,11 @@ pub enum BlockKind { | |
| /// Tool name. | ||
| name: String, | ||
| }, | ||
| /// An opaque provider-defined content block. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reconstruct provider-extension deltas before completing the stream Adding a provider-extension block kind without a corresponding incremental representation leaves extension fragments unable to participate in generic stream reconstruction. When a stream completes without an authoritative response, the accumulator cannot reconstruct the extension content from [RULE] stream-extension-reconstruction · There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate provider extension fields before forwarding This newly exposed provider-extension variant accepts an arbitrary provider-supplied [RULE] provider-extension-validation · |
||
| ProviderExtension { | ||
| /// Provider wire type for the extension block. | ||
| block_type: String, | ||
| }, | ||
| } | ||
|
|
||
| /// An incremental fragment belonging to the open block named in the | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -154,6 +154,7 @@ pub(crate) fn request_body(request: &ModelRequest, default_model: &str) -> Value | |
| ReasoningEffort::Low => "low", | ||
| ReasoningEffort::Medium => "medium", | ||
| ReasoningEffort::High => "high", | ||
| ReasoningEffort::XHigh => "max", | ||
| ReasoningEffort::None => unreachable!(), | ||
| }, | ||
| }); | ||
|
|
@@ -229,12 +230,11 @@ fn content_blocks(content: &[ContentBlock]) -> Vec<Value> { | |
| ContentBlock::Text(text) => text_block(text), | ||
| ContentBlock::Json(value) => text_block(&value.to_string()), | ||
| ContentBlock::Image(image) => Some(image_block(image)), | ||
| ContentBlock::ProviderExtension(value) => provider_extension_block(value), | ||
|
senamakel marked this conversation as resolved.
|
||
| ContentBlock::Document(media) => Some(document_block(media)), | ||
| ContentBlock::Audio(media) => Some(unsupported_media_placeholder("audio", media)), | ||
| ContentBlock::Video(media) => Some(unsupported_media_placeholder("video", media)), | ||
| ContentBlock::Thinking { .. } | ||
| | ContentBlock::RedactedThinking { .. } | ||
| | ContentBlock::ProviderExtension(_) => None, | ||
| ContentBlock::Thinking { .. } | ContentBlock::RedactedThinking { .. } => None, | ||
| }) | ||
| .collect() | ||
| } | ||
|
|
@@ -260,18 +260,32 @@ fn assistant_blocks(content: &[ContentBlock]) -> Vec<Value> { | |
| ContentBlock::RedactedThinking { data } => { | ||
| Some(json!({ "type": "redacted_thinking", "data": data })) | ||
| } | ||
| ContentBlock::ProviderExtension(value) => provider_extension_block(value), | ||
| ContentBlock::Thinking { | ||
| signature: None, .. | ||
| } | ||
| | ContentBlock::Image(_) | ||
| | ContentBlock::ProviderExtension(_) | ||
| | ContentBlock::Audio(_) | ||
| | ContentBlock::Video(_) | ||
| | ContentBlock::Document(_) => None, | ||
| | ContentBlock::Image(_) => None, | ||
| ContentBlock::Audio(_) | ContentBlock::Video(_) | ContentBlock::Document(_) => None, | ||
| }) | ||
| .collect() | ||
| } | ||
|
|
||
| /// Returns an opaque Anthropic content block when it has the object shape the | ||
| /// Messages API requires. Keeping the full object intact lets hosts persist and | ||
| /// replay newer block types without waiting for a TinyInference release. | ||
| fn provider_extension_block(value: &Value) -> Option<Value> { | ||
| let object = value.as_object()?; | ||
| let block_type = object.get("type").and_then(Value::as_str)?; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate required fields before forwarding extensions This accepts every unknown block containing only a string [RULE] validate-provider-extension · |
||
| // Provider extensions are for block types this adapter does not model. | ||
| // Rejecting native types prevents callers from bypassing their normalized | ||
| // representations with incomplete provider-shaped JSON. | ||
| (!matches!( | ||
| block_type, | ||
| "text" | "image" | "document" | "tool_use" | "thinking" | "redacted_thinking" | ||
| )) | ||
| .then(|| value.clone()) | ||
| } | ||
|
|
||
| /// Renders an image reference: a `data:` URI becomes an inline base64 source, | ||
| /// anything else a URL source. | ||
| fn image_block(image: &ImageRef) -> Value { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,10 @@ enum OpenBlock { | |
| signature: Option<String>, | ||
| }, | ||
| Redacted(String), | ||
| ProviderExtension { | ||
| value: Value, | ||
| partial_json: String, | ||
| }, | ||
| } | ||
|
|
||
| impl OpenBlock { | ||
|
|
@@ -87,10 +91,23 @@ impl OpenBlock { | |
| "arguments": arguments, | ||
| })) | ||
| } | ||
| OpenBlock::ProviderExtension { | ||
| value, | ||
| partial_json, | ||
| } => ContentBlock::ProviderExtension(provider_extension_value(value, partial_json)), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn provider_extension_value(mut value: Value, partial_json: String) -> Value { | ||
| if !partial_json.trim().is_empty() | ||
| && let Ok(input) = serde_json::from_str::<Value>(&partial_json) | ||
| { | ||
| value["input"] = input; | ||
| } | ||
| value | ||
| } | ||
|
|
||
| /// Provider-side accumulator rebuilding the terminal [`ModelResponse`]. | ||
| #[derive(Debug, Default)] | ||
| struct AnthropicStreamAcc { | ||
|
|
@@ -201,7 +218,7 @@ impl AnthropicStreamAcc { | |
| }); | ||
| OpenBlock::Redacted(block["data"].as_str().unwrap_or_default().to_string()) | ||
| } | ||
| _ => { | ||
| Some("text") => { | ||
| pending.push_back(ModelStreamItem::BlockStart { | ||
| index, | ||
| kind: BlockKind::Text, | ||
|
|
@@ -218,6 +235,18 @@ impl AnthropicStreamAcc { | |
| } | ||
| OpenBlock::Text(text) | ||
| } | ||
| _ => { | ||
| pending.push_back(ModelStreamItem::BlockStart { | ||
| index, | ||
| kind: BlockKind::ProviderExtension { | ||
| block_type: block["type"].as_str().unwrap_or_default().to_string(), | ||
| }, | ||
| }); | ||
| OpenBlock::ProviderExtension { | ||
| value: block.clone(), | ||
| partial_json: String::new(), | ||
| } | ||
| } | ||
| }; | ||
| *self.slot(index) = Some(open); | ||
| } | ||
|
|
@@ -258,6 +287,13 @@ impl AnthropicStreamAcc { | |
| content_index: Some(index), | ||
| })); | ||
| } | ||
| ( | ||
| Some("input_json_delta"), | ||
| Some(OpenBlock::ProviderExtension { partial_json, .. }), | ||
| ) => { | ||
| let fragment = delta["partial_json"].as_str().unwrap_or_default(); | ||
| partial_json.push_str(fragment); | ||
|
Comment on lines
+294
to
+295
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an Anthropic AGENTS.md reference: AGENTS.md:L46-L48 Useful? React with 👍 / 👎. |
||
| } | ||
| (Some("thinking_delta"), Some(OpenBlock::Thinking { text, .. })) => { | ||
| let fragment = delta["thinking"].as_str().unwrap_or_default(); | ||
| text.push_str(fragment); | ||
|
|
@@ -339,6 +375,15 @@ impl AnthropicStreamAcc { | |
| content.push(ContentBlock::Thinking { text, signature }); | ||
| } | ||
| OpenBlock::Redacted(data) => content.push(ContentBlock::RedactedThinking { data }), | ||
| OpenBlock::ProviderExtension { | ||
| value, | ||
| partial_json, | ||
| } => { | ||
| content.push(ContentBlock::ProviderExtension(provider_extension_value( | ||
| value, | ||
| partial_json, | ||
| ))); | ||
| } | ||
| OpenBlock::ToolUse { | ||
| id, | ||
| name, | ||
|
|
@@ -371,6 +416,7 @@ impl AnthropicStreamAcc { | |
| "stop_reason": self.stop_reason, | ||
| "content": content.iter().filter_map(|block| match block { | ||
| ContentBlock::Text(text) => Some(serde_json::json!({"type": "text", "text": text})), | ||
| ContentBlock::ProviderExtension(value) => Some(value.clone()), | ||
| _ => None, | ||
| }).chain(tool_calls.iter().map(|call| serde_json::json!({ | ||
| "type": "tool_use", "id": call.id, "name": call.name, "input": call.arguments, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accumulate deltas for provider extensions
The new extension block can be opened, but
BlockDeltahas no provider-extension payload variant. Incremental extension data therefore cannot be emitted and accumulated through the normalized stream protocol, risking loss of provider content for consumers that rely on deltas. Add an extension delta representation and fold it into the completed block.[RULE] stream-extension-accumulation ·