mirror of
https://github.com/instructkr/claw-code.git
synced 2026-05-18 21:41:26 +08:00
omx(team): merge worker-1
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::{BTreeMap, VecDeque};
|
use std::collections::{BTreeMap, VecDeque};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
@@ -267,14 +268,18 @@ impl OpenAiCompatClient {
|
|||||||
request: &MessageRequest,
|
request: &MessageRequest,
|
||||||
) -> Result<reqwest::Response, ApiError> {
|
) -> Result<reqwest::Response, ApiError> {
|
||||||
// Pre-flight check: verify request body size against provider limits
|
// Pre-flight check: verify request body size against provider limits
|
||||||
check_request_body_size(request, self.config())?;
|
check_request_body_size_for_base_url(request, self.config(), &self.base_url)?;
|
||||||
|
|
||||||
let request_url = chat_completions_endpoint(&self.base_url);
|
let request_url = chat_completions_endpoint(&self.base_url);
|
||||||
self.http
|
self.http
|
||||||
.post(&request_url)
|
.post(&request_url)
|
||||||
.header("content-type", "application/json")
|
.header("content-type", "application/json")
|
||||||
.bearer_auth(&self.api_key)
|
.bearer_auth(&self.api_key)
|
||||||
.json(&build_chat_completion_request(request, self.config()))
|
.json(&build_chat_completion_request_for_base_url(
|
||||||
|
request,
|
||||||
|
self.config(),
|
||||||
|
&self.base_url,
|
||||||
|
))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
@@ -882,10 +887,50 @@ fn strip_routing_prefix(model: &str) -> &str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wire_model_for_base_url<'a>(
|
||||||
|
model: &'a str,
|
||||||
|
config: OpenAiCompatConfig,
|
||||||
|
base_url: &str,
|
||||||
|
) -> Cow<'a, str> {
|
||||||
|
let Some(pos) = model.find('/') else {
|
||||||
|
return Cow::Borrowed(model);
|
||||||
|
};
|
||||||
|
let prefix = &model[..pos];
|
||||||
|
let lowered_prefix = prefix.to_ascii_lowercase();
|
||||||
|
|
||||||
|
if lowered_prefix == "openai" {
|
||||||
|
let trimmed_base_url = base_url.trim_end_matches('/');
|
||||||
|
let default_openai = DEFAULT_OPENAI_BASE_URL.trim_end_matches('/');
|
||||||
|
if config.provider_name == "OpenAI" && trimmed_base_url != default_openai {
|
||||||
|
// OpenAI-compatible gateways such as OpenRouter commonly use
|
||||||
|
// slash-containing model slugs (for example `openai/gpt-4.1-mini`).
|
||||||
|
// Preserve the slug when the user configured a non-default OpenAI
|
||||||
|
// base URL; the prefix still routed to the OpenAI-compatible client,
|
||||||
|
// but the gateway owns the final model namespace.
|
||||||
|
return Cow::Borrowed(model);
|
||||||
|
}
|
||||||
|
return Cow::Borrowed(&model[pos + 1..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(lowered_prefix.as_str(), "xai" | "grok" | "qwen" | "kimi") {
|
||||||
|
return Cow::Borrowed(&model[pos + 1..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cow::Borrowed(model)
|
||||||
|
}
|
||||||
|
|
||||||
/// Estimate the serialized JSON size of a request payload in bytes.
|
/// Estimate the serialized JSON size of a request payload in bytes.
|
||||||
/// This is a pre-flight check to avoid hitting provider-specific size limits.
|
/// This is a pre-flight check to avoid hitting provider-specific size limits.
|
||||||
pub fn estimate_request_body_size(request: &MessageRequest, config: OpenAiCompatConfig) -> usize {
|
pub fn estimate_request_body_size(request: &MessageRequest, config: OpenAiCompatConfig) -> usize {
|
||||||
let payload = build_chat_completion_request(request, config);
|
estimate_request_body_size_for_base_url(request, config, &read_base_url(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_request_body_size_for_base_url(
|
||||||
|
request: &MessageRequest,
|
||||||
|
config: OpenAiCompatConfig,
|
||||||
|
base_url: &str,
|
||||||
|
) -> usize {
|
||||||
|
let payload = build_chat_completion_request_for_base_url(request, config, base_url);
|
||||||
// serde_json::to_vec gives us the exact byte size of the serialized JSON
|
// serde_json::to_vec gives us the exact byte size of the serialized JSON
|
||||||
serde_json::to_vec(&payload).map_or(0, |v| v.len())
|
serde_json::to_vec(&payload).map_or(0, |v| v.len())
|
||||||
}
|
}
|
||||||
@@ -897,7 +942,15 @@ pub fn check_request_body_size(
|
|||||||
request: &MessageRequest,
|
request: &MessageRequest,
|
||||||
config: OpenAiCompatConfig,
|
config: OpenAiCompatConfig,
|
||||||
) -> Result<(), ApiError> {
|
) -> Result<(), ApiError> {
|
||||||
let estimated_bytes = estimate_request_body_size(request, config);
|
check_request_body_size_for_base_url(request, config, &read_base_url(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_request_body_size_for_base_url(
|
||||||
|
request: &MessageRequest,
|
||||||
|
config: OpenAiCompatConfig,
|
||||||
|
base_url: &str,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
let estimated_bytes = estimate_request_body_size_for_base_url(request, config, base_url);
|
||||||
let max_bytes = config.max_request_body_bytes;
|
let max_bytes = config.max_request_body_bytes;
|
||||||
|
|
||||||
if estimated_bytes > max_bytes {
|
if estimated_bytes > max_bytes {
|
||||||
@@ -916,6 +969,14 @@ pub fn check_request_body_size(
|
|||||||
pub fn build_chat_completion_request(
|
pub fn build_chat_completion_request(
|
||||||
request: &MessageRequest,
|
request: &MessageRequest,
|
||||||
config: OpenAiCompatConfig,
|
config: OpenAiCompatConfig,
|
||||||
|
) -> Value {
|
||||||
|
build_chat_completion_request_for_base_url(request, config, &read_base_url(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_chat_completion_request_for_base_url(
|
||||||
|
request: &MessageRequest,
|
||||||
|
config: OpenAiCompatConfig,
|
||||||
|
base_url: &str,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
let mut messages = Vec::new();
|
let mut messages = Vec::new();
|
||||||
if let Some(system) = request.system.as_ref().filter(|value| !value.is_empty()) {
|
if let Some(system) = request.system.as_ref().filter(|value| !value.is_empty()) {
|
||||||
@@ -924,8 +985,10 @@ pub fn build_chat_completion_request(
|
|||||||
"content": system,
|
"content": system,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
// Strip routing prefix (e.g., "openai/gpt-4" → "gpt-4") for the wire.
|
// Resolve the transport routing prefix into the wire model. Custom
|
||||||
let wire_model = strip_routing_prefix(&request.model);
|
// OpenAI-compatible gateways may require slash-containing slugs intact.
|
||||||
|
let wire_model = wire_model_for_base_url(&request.model, config, base_url);
|
||||||
|
let wire_model = wire_model.as_ref();
|
||||||
for message in &request.messages {
|
for message in &request.messages {
|
||||||
messages.extend(translate_message(message, wire_model));
|
messages.extend(translate_message(message, wire_model));
|
||||||
}
|
}
|
||||||
@@ -994,9 +1057,29 @@ pub fn build_chat_completion_request(
|
|||||||
payload["reasoning_effort"] = json!(effort);
|
payload["reasoning_effort"] = json!(effort);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (key, value) in &request.extra_body {
|
||||||
|
if is_protected_extra_body_key(key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
payload[key] = value.clone();
|
||||||
|
}
|
||||||
|
|
||||||
payload
|
payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_protected_extra_body_key(key: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
key,
|
||||||
|
"model"
|
||||||
|
| "messages"
|
||||||
|
| "stream"
|
||||||
|
| "tools"
|
||||||
|
| "tool_choice"
|
||||||
|
| "max_tokens"
|
||||||
|
| "max_completion_tokens"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true for models that do NOT support the `is_error` field in tool results.
|
/// Returns true for models that do NOT support the `is_error` field in tool results.
|
||||||
/// kimi models (via Moonshot AI/Dashscope) reject this field with 400 Bad Request.
|
/// kimi models (via Moonshot AI/Dashscope) reject this field with 400 Bad Request.
|
||||||
/// Returns true for models that do NOT support the `is_error` field in tool results.
|
/// Returns true for models that do NOT support the `is_error` field in tool results.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use runtime::{pricing_for_model, TokenUsage, UsageCostEstimate};
|
use runtime::{pricing_for_model, TokenUsage, UsageCostEstimate};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -31,6 +33,14 @@ pub struct MessageRequest {
|
|||||||
/// Silently ignored by backends that do not support it.
|
/// Silently ignored by backends that do not support it.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub reasoning_effort: Option<String>,
|
pub reasoning_effort: Option<String>,
|
||||||
|
/// Provider-specific OpenAI-compatible request body parameters. These are
|
||||||
|
/// copied into the final JSON payload after core fields are populated so
|
||||||
|
/// users can opt into gateway features such as `web_search_options`,
|
||||||
|
/// `parallel_tool_calls`, or custom local-server switches without waiting
|
||||||
|
/// for first-class typed fields. Core protocol keys are protected and cannot
|
||||||
|
/// be overridden through this map.
|
||||||
|
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||||
|
pub extra_body: BTreeMap<String, Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageRequest {
|
impl MessageRequest {
|
||||||
|
|||||||
Reference in New Issue
Block a user