1use crate::ollama_chat::{
6 BoundedRetryAfter, HttpMethod, NetworkPolicy, ReplayBoundary, ReplayDecision,
7};
8use router_protocol::{
9 AttemptId, CanonicalRequest, ContentBlock, InputItem, MessageRole, PhaseKind, RouteIdentity,
10 RouteIdentityError, ToolChoice, TransportKind,
11};
12use serde_json::{Map, Value, json};
13use std::collections::{BTreeMap, HashSet};
14use std::fmt;
15
16pub const DEFAULT_MAX_SSE_EVENT_BYTES: usize = 256 * 1024;
17
18#[derive(Clone, PartialEq, Eq)]
19pub struct OpenRouterSecretReferenceMarker(String);
20
21impl OpenRouterSecretReferenceMarker {
22 pub fn as_str(&self) -> &str {
23 &self.0
24 }
25}
26
27impl fmt::Debug for OpenRouterSecretReferenceMarker {
28 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29 formatter.write_str("OpenRouterSecretReferenceMarker([REDACTED])")
30 }
31}
32
33#[derive(Clone, PartialEq)]
34pub struct OpenRouterRequestSpec {
35 pub method: HttpMethod,
36 pub origin: String,
37 pub path: String,
38 pub content_type: &'static str,
39 pub accept: &'static str,
40 pub bearer_secret: OpenRouterSecretReferenceMarker,
41 pub network: NetworkPolicy,
42 body: Value,
43}
44
45impl OpenRouterRequestSpec {
46 pub fn body(&self) -> &Value {
47 &self.body
48 }
49
50 pub fn body_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
51 serde_json::to_vec(&self.body)
52 }
53}
54
55impl fmt::Debug for OpenRouterRequestSpec {
56 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57 formatter
58 .debug_struct("OpenRouterRequestSpec")
59 .field("method", &self.method)
60 .field("origin", &self.origin)
61 .field("path", &self.path)
62 .field("content_type", &self.content_type)
63 .field("accept", &self.accept)
64 .field("bearer_secret", &self.bearer_secret)
65 .field("network", &self.network)
66 .field("body", &"[REDACTED]")
67 .finish()
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum OpenRouterRequestErrorKind {
73 InvalidRouteIdentity,
74 UnsupportedRoute,
75 InvalidCanonicalRequest,
76 AmbiguousToolName,
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct OpenRouterRequestError {
81 pub kind: OpenRouterRequestErrorKind,
82}
83
84impl fmt::Display for OpenRouterRequestError {
85 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86 formatter.write_str("the OpenRouter chat request could not be prepared")
87 }
88}
89
90impl std::error::Error for OpenRouterRequestError {}
91
92impl From<RouteIdentityError> for OpenRouterRequestError {
93 fn from(_: RouteIdentityError) -> Self {
94 Self {
95 kind: OpenRouterRequestErrorKind::InvalidRouteIdentity,
96 }
97 }
98}
99
100pub fn build_request(
101 route: &RouteIdentity,
102 request: &CanonicalRequest,
103) -> Result<OpenRouterRequestSpec, OpenRouterRequestError> {
104 route.validate()?;
105 if route.phase != PhaseKind::OpenrouterApi || route.transport != TransportKind::DirectHttps {
106 return Err(OpenRouterRequestError {
107 kind: OpenRouterRequestErrorKind::UnsupportedRoute,
108 });
109 }
110 request.validate().map_err(|_| OpenRouterRequestError {
111 kind: OpenRouterRequestErrorKind::InvalidCanonicalRequest,
112 })?;
113 let secret_ref = route.credential_ref.clone().ok_or(OpenRouterRequestError {
114 kind: OpenRouterRequestErrorKind::InvalidRouteIdentity,
115 })?;
116
117 Ok(OpenRouterRequestSpec {
118 method: HttpMethod::Post,
119 origin: route.endpoint.origin.clone(),
120 path: route.endpoint.path.clone(),
121 content_type: "application/json",
122 accept: "text/event-stream",
123 bearer_secret: OpenRouterSecretReferenceMarker(secret_ref),
124 network: NetworkPolicy {
125 allow_proxy: false,
126 allow_dns: true,
127 follow_redirects: false,
128 require_tls: true,
129 },
130 body: build_body(route, request)?,
131 })
132}
133
134fn build_body(
135 route: &RouteIdentity,
136 request: &CanonicalRequest,
137) -> Result<Value, OpenRouterRequestError> {
138 let mut object = Map::new();
139 object.insert("model".to_owned(), Value::String(route.model_id.clone()));
140 object.insert("messages".to_owned(), Value::Array(build_messages(request)));
141 object.insert("stream".to_owned(), Value::Bool(true));
142 object.insert("stream_options".to_owned(), json!({"include_usage": true}));
143 object.insert(
144 "provider".to_owned(),
145 json!({
146 "require_parameters": true,
147 "allow_fallbacks": false,
148 "data_collection": "deny",
149 "zdr": true,
150 }),
151 );
152 object.insert("tool_choice".to_owned(), openrouter_tool_choice(request));
153
154 if !request.tools.is_empty() && request.tool_choice != ToolChoice::None {
155 let mut names = HashSet::new();
156 let mut tools = Vec::with_capacity(request.tools.len());
157 for tool in &request.tools {
158 let name = provider_tool_name(tool.namespace.as_deref(), &tool.name);
159 if !names.insert(name.clone()) {
160 return Err(OpenRouterRequestError {
161 kind: OpenRouterRequestErrorKind::AmbiguousToolName,
162 });
163 }
164 tools.push(json!({
165 "type": "function",
166 "function": {
167 "name": name,
168 "description": tool.description,
169 "parameters": tool.parameters,
170 "strict": tool.strict,
171 }
172 }));
173 }
174 object.insert("tools".to_owned(), Value::Array(tools));
175 }
176 if let Some(maximum) = request.max_output_tokens {
177 object.insert("max_tokens".to_owned(), Value::from(maximum));
178 }
179 if let Some(format) = &request.structured_output {
180 object.insert(
181 "response_format".to_owned(),
182 json!({
183 "type": "json_schema",
184 "json_schema": {
185 "name": format.name,
186 "strict": format.strict,
187 "schema": format.schema,
188 }
189 }),
190 );
191 }
192 Ok(Value::Object(object))
193}
194
195fn build_messages(request: &CanonicalRequest) -> Vec<Value> {
196 request
197 .input
198 .iter()
199 .map(|item| match item {
200 InputItem::Message { role, content } => json!({
201 "role": role_name(*role),
202 "content": openrouter_content(content),
203 }),
204 InputItem::FunctionCall {
205 call_id,
206 namespace,
207 name,
208 arguments,
209 } => json!({
210 "role": "assistant",
211 "content": null,
212 "tool_calls": [{
213 "id": call_id.as_str(),
214 "type": "function",
215 "function": {
216 "name": provider_tool_name(namespace.as_deref(), name),
217 "arguments": arguments.to_string(),
218 }
219 }]
220 }),
221 InputItem::FunctionCallOutput { call_id, output } => json!({
222 "role": "tool",
223 "tool_call_id": call_id.as_str(),
224 "content": output.to_string(),
225 }),
226 })
227 .collect()
228}
229
230fn openrouter_content(content: &[ContentBlock]) -> Value {
231 if content
232 .iter()
233 .all(|block| matches!(block, ContentBlock::Text { .. }))
234 {
235 let text = content
236 .iter()
237 .filter_map(|block| match block {
238 ContentBlock::Text { text } => Some(text.as_str()),
239 ContentBlock::ImageReference { .. } => None,
240 })
241 .collect::<String>();
242 return Value::String(text);
243 }
244 Value::Array(
245 content
246 .iter()
247 .map(|block| match block {
248 ContentBlock::Text { text } => json!({"type": "text", "text": text}),
249 ContentBlock::ImageReference { url } => {
250 json!({"type": "image_url", "image_url": {"url": url}})
251 }
252 })
253 .collect(),
254 )
255}
256
257const fn role_name(role: MessageRole) -> &'static str {
258 match role {
259 MessageRole::Developer => "developer",
260 MessageRole::System => "system",
261 MessageRole::User => "user",
262 MessageRole::Assistant => "assistant",
263 }
264}
265
266fn provider_tool_name(namespace: Option<&str>, name: &str) -> String {
267 namespace.map_or_else(
268 || name.to_owned(),
269 |namespace| format!("{namespace}__{name}"),
270 )
271}
272
273fn openrouter_tool_choice(request: &CanonicalRequest) -> Value {
274 match &request.tool_choice {
275 ToolChoice::Auto => Value::String("auto".to_owned()),
276 ToolChoice::None => Value::String("none".to_owned()),
277 ToolChoice::Required => Value::String("required".to_owned()),
278 ToolChoice::Named(name) => {
279 let provider_name = request
280 .tools
281 .iter()
282 .find(|tool| tool.name == *name)
283 .map_or_else(
284 || name.clone(),
285 |tool| provider_tool_name(tool.namespace.as_deref(), &tool.name),
286 );
287 json!({"type":"function","function":{"name":provider_name}})
288 }
289 }
290}
291
292#[derive(Clone, Debug, PartialEq, Eq)]
293pub struct OpenRouterUsage {
294 pub prompt_tokens: u64,
295 pub completion_tokens: u64,
296 pub total_tokens: u64,
297}
298
299#[derive(Clone, Debug, PartialEq, Eq)]
300pub enum OpenRouterStreamEvent {
301 ContentDelta {
302 text: String,
303 },
304 ToolCallDelta {
305 id: String,
306 ordinal: u64,
307 index: u64,
308 name: Option<String>,
309 arguments: Option<String>,
310 },
311 FinishReason {
312 reason: String,
313 },
314 Usage(OpenRouterUsage),
315 Done,
316}
317
318#[derive(Clone, Copy, Debug, PartialEq, Eq)]
319pub enum OpenRouterStreamErrorKind {
320 InvalidUtf8,
321 InvalidJson,
322 InvalidFrame,
323 EventTooLarge,
324 ProviderError,
325 MissingUsage,
326 DuplicateUsage,
327 MissingTerminal,
328 DuplicateTerminal,
329 DataAfterTerminal,
330 ParserFailed,
331}
332
333#[derive(Clone, Copy, Debug, PartialEq, Eq)]
334pub struct OpenRouterStreamError {
335 pub kind: OpenRouterStreamErrorKind,
336 pub byte_offset: u64,
337}
338
339impl fmt::Display for OpenRouterStreamError {
340 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
341 formatter.write_str("the OpenRouter response stream was invalid")
342 }
343}
344
345impl std::error::Error for OpenRouterStreamError {}
346
347#[derive(Clone, Debug)]
348struct ToolState {
349 id: String,
350 ordinal: u64,
351}
352
353pub struct OpenRouterSseParser {
354 attempt_ref: String,
355 line_buffer: Vec<u8>,
356 data_lines: Vec<String>,
357 data_bytes: usize,
358 max_event_bytes: usize,
359 byte_offset: u64,
360 tool_states: BTreeMap<u64, ToolState>,
361 next_tool_ordinal: u64,
362 usage_seen: bool,
363 terminal: bool,
364 failed: bool,
365 replay_guard: OpenRouterReplayGuard,
366}
367
368impl fmt::Debug for OpenRouterSseParser {
369 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
370 formatter
371 .debug_struct("OpenRouterSseParser")
372 .field("buffered_line_bytes", &self.line_buffer.len())
373 .field("data_bytes", &self.data_bytes)
374 .field("max_event_bytes", &self.max_event_bytes)
375 .field("byte_offset", &self.byte_offset)
376 .field("tool_count", &self.tool_states.len())
377 .field("usage_seen", &self.usage_seen)
378 .field("terminal", &self.terminal)
379 .field("failed", &self.failed)
380 .field("replay_decision", &self.replay_guard.decision())
381 .finish()
382 }
383}
384
385impl OpenRouterSseParser {
386 pub fn new(attempt_id: &AttemptId) -> Self {
387 Self::with_max_event_bytes(attempt_id, DEFAULT_MAX_SSE_EVENT_BYTES)
388 }
389
390 pub fn with_max_event_bytes(attempt_id: &AttemptId, max_event_bytes: usize) -> Self {
391 Self {
392 attempt_ref: attempt_id.as_str().to_owned(),
393 line_buffer: Vec::new(),
394 data_lines: Vec::new(),
395 data_bytes: 0,
396 max_event_bytes: max_event_bytes.max(1),
397 byte_offset: 0,
398 tool_states: BTreeMap::new(),
399 next_tool_ordinal: 0,
400 usage_seen: false,
401 terminal: false,
402 failed: false,
403 replay_guard: OpenRouterReplayGuard::default(),
404 }
405 }
406
407 pub fn mark_body_written(&mut self) {
408 self.replay_guard.mark_body_written();
409 }
410
411 pub fn mark_forwarded_tool_result(&mut self) {
412 self.replay_guard.mark_forwarded_tool_result();
413 }
414
415 pub fn mark_transport_outcome_unknown(&mut self) {
416 self.replay_guard.mark_transport_outcome_unknown();
417 }
418
419 pub fn replay_decision(&self) -> ReplayDecision {
420 self.replay_guard.decision()
421 }
422
423 pub fn feed(
424 &mut self,
425 bytes: &[u8],
426 ) -> Result<Vec<OpenRouterStreamEvent>, OpenRouterStreamError> {
427 if self.failed {
428 return Err(self.error(OpenRouterStreamErrorKind::ParserFailed));
429 }
430 let mut events = Vec::new();
431 for &byte in bytes {
432 self.byte_offset = self.byte_offset.saturating_add(1);
433 if byte == b'\n' {
434 let mut line = std::mem::take(&mut self.line_buffer);
435 if line.last() == Some(&b'\r') {
436 line.pop();
437 }
438 if let Err(error) = self.process_line(&line, &mut events) {
439 self.failed = true;
440 return Err(error);
441 }
442 } else {
443 self.line_buffer.push(byte);
444 if self.line_buffer.len() > self.max_event_bytes {
445 self.failed = true;
446 return Err(self.error(OpenRouterStreamErrorKind::EventTooLarge));
447 }
448 }
449 }
450 Ok(events)
451 }
452
453 pub fn finish(&mut self) -> Result<Vec<OpenRouterStreamEvent>, OpenRouterStreamError> {
454 if self.failed {
455 return Err(self.error(OpenRouterStreamErrorKind::ParserFailed));
456 }
457 let mut events = Vec::new();
458 if !self.line_buffer.is_empty() {
459 let mut line = std::mem::take(&mut self.line_buffer);
460 if line.last() == Some(&b'\r') {
461 line.pop();
462 }
463 if let Err(error) = self.process_line(&line, &mut events) {
464 self.failed = true;
465 return Err(error);
466 }
467 }
468 if !self.data_lines.is_empty()
469 && let Err(error) = self.dispatch_event(&mut events)
470 {
471 self.failed = true;
472 return Err(error);
473 }
474 if !self.terminal {
475 self.failed = true;
476 return Err(self.error(OpenRouterStreamErrorKind::MissingTerminal));
477 }
478 Ok(events)
479 }
480
481 fn process_line(
482 &mut self,
483 line: &[u8],
484 events: &mut Vec<OpenRouterStreamEvent>,
485 ) -> Result<(), OpenRouterStreamError> {
486 let line = std::str::from_utf8(line)
487 .map_err(|_| self.error(OpenRouterStreamErrorKind::InvalidUtf8))?;
488 if line.is_empty() {
489 return self.dispatch_event(events);
490 }
491 if line.starts_with(':') {
492 return Ok(());
493 }
494 let (field, value) = line.split_once(':').unwrap_or((line, ""));
495 if field != "data" {
496 return Ok(());
497 }
498 let value = value.strip_prefix(' ').unwrap_or(value);
499 let separator = usize::from(!self.data_lines.is_empty());
500 self.data_bytes = self
501 .data_bytes
502 .saturating_add(value.len())
503 .saturating_add(separator);
504 if self.data_bytes > self.max_event_bytes {
505 return Err(self.error(OpenRouterStreamErrorKind::EventTooLarge));
506 }
507 self.data_lines.push(value.to_owned());
508 Ok(())
509 }
510
511 fn dispatch_event(
512 &mut self,
513 events: &mut Vec<OpenRouterStreamEvent>,
514 ) -> Result<(), OpenRouterStreamError> {
515 if self.data_lines.is_empty() {
516 return Ok(());
517 }
518 let data = self.data_lines.join("\n");
519 self.data_lines.clear();
520 self.data_bytes = 0;
521 if data == "[DONE]" {
522 if self.terminal {
523 return Err(self.error(OpenRouterStreamErrorKind::DuplicateTerminal));
524 }
525 if !self.usage_seen {
526 return Err(self.error(OpenRouterStreamErrorKind::MissingUsage));
527 }
528 self.terminal = true;
529 events.push(OpenRouterStreamEvent::Done);
530 return Ok(());
531 }
532 if self.terminal {
533 return Err(self.error(OpenRouterStreamErrorKind::DataAfterTerminal));
534 }
535 let value: Value = serde_json::from_str(&data)
536 .map_err(|_| self.error(OpenRouterStreamErrorKind::InvalidJson))?;
537 let object = value
538 .as_object()
539 .ok_or_else(|| self.error(OpenRouterStreamErrorKind::InvalidFrame))?;
540 if object.contains_key("error") {
541 return Err(self.error(OpenRouterStreamErrorKind::ProviderError));
542 }
543 self.parse_usage(object, events)?;
544 self.parse_choices(object, events)
545 }
546
547 fn parse_usage(
548 &mut self,
549 object: &Map<String, Value>,
550 events: &mut Vec<OpenRouterStreamEvent>,
551 ) -> Result<(), OpenRouterStreamError> {
552 let Some(usage) = object.get("usage") else {
553 return Ok(());
554 };
555 if usage.is_null() {
556 return Ok(());
557 }
558 if self.usage_seen {
559 return Err(self.error(OpenRouterStreamErrorKind::DuplicateUsage));
560 }
561 let usage = usage
562 .as_object()
563 .ok_or_else(|| self.error(OpenRouterStreamErrorKind::InvalidFrame))?;
564 let prompt_tokens = required_u64(usage, "prompt_tokens", self.byte_offset)?;
565 let completion_tokens = required_u64(usage, "completion_tokens", self.byte_offset)?;
566 let total_tokens = required_u64(usage, "total_tokens", self.byte_offset)?;
567 self.usage_seen = true;
568 events.push(OpenRouterStreamEvent::Usage(OpenRouterUsage {
569 prompt_tokens,
570 completion_tokens,
571 total_tokens,
572 }));
573 Ok(())
574 }
575
576 fn parse_choices(
577 &mut self,
578 object: &Map<String, Value>,
579 events: &mut Vec<OpenRouterStreamEvent>,
580 ) -> Result<(), OpenRouterStreamError> {
581 let Some(choices) = object.get("choices") else {
582 return Ok(());
583 };
584 let choices = choices
585 .as_array()
586 .ok_or_else(|| self.error(OpenRouterStreamErrorKind::InvalidFrame))?;
587 for choice in choices {
588 let choice = choice
589 .as_object()
590 .ok_or_else(|| self.error(OpenRouterStreamErrorKind::InvalidFrame))?;
591 if choice.contains_key("error") {
592 return Err(self.error(OpenRouterStreamErrorKind::ProviderError));
593 }
594 if let Some(delta) = choice.get("delta") {
595 let delta = delta
596 .as_object()
597 .ok_or_else(|| self.error(OpenRouterStreamErrorKind::InvalidFrame))?;
598 if delta.contains_key("error") {
599 return Err(self.error(OpenRouterStreamErrorKind::ProviderError));
600 }
601 if let Some(content) = delta.get("content")
602 && !content.is_null()
603 {
604 let text = content
605 .as_str()
606 .ok_or_else(|| self.error(OpenRouterStreamErrorKind::InvalidFrame))?;
607 if !text.is_empty() {
608 let event = OpenRouterStreamEvent::ContentDelta {
609 text: text.to_owned(),
610 };
611 self.replay_guard.observe(&event);
612 events.push(event);
613 }
614 }
615 if let Some(tool_calls) = delta.get("tool_calls") {
616 self.parse_tool_calls(tool_calls, events)?;
617 }
618 }
619 if let Some(reason) = choice.get("finish_reason")
620 && !reason.is_null()
621 {
622 let reason = reason
623 .as_str()
624 .ok_or_else(|| self.error(OpenRouterStreamErrorKind::InvalidFrame))?;
625 if reason == "error" {
626 return Err(self.error(OpenRouterStreamErrorKind::ProviderError));
627 }
628 events.push(OpenRouterStreamEvent::FinishReason {
629 reason: reason.to_owned(),
630 });
631 }
632 }
633 Ok(())
634 }
635
636 fn parse_tool_calls(
637 &mut self,
638 value: &Value,
639 events: &mut Vec<OpenRouterStreamEvent>,
640 ) -> Result<(), OpenRouterStreamError> {
641 let calls = value
642 .as_array()
643 .ok_or_else(|| self.error(OpenRouterStreamErrorKind::InvalidFrame))?;
644 for call in calls {
645 let call = call
646 .as_object()
647 .ok_or_else(|| self.error(OpenRouterStreamErrorKind::InvalidFrame))?;
648 let index = call
649 .get("index")
650 .and_then(Value::as_u64)
651 .ok_or_else(|| self.error(OpenRouterStreamErrorKind::InvalidFrame))?;
652 let incoming_id = call.get("id").and_then(Value::as_str);
653 let state = if let Some(state) = self.tool_states.get(&index) {
654 if incoming_id.is_some_and(|id| id != state.id) {
655 return Err(self.error(OpenRouterStreamErrorKind::InvalidFrame));
656 }
657 state.clone()
658 } else {
659 let ordinal = self.next_tool_ordinal;
660 self.next_tool_ordinal = self.next_tool_ordinal.saturating_add(1);
661 let state = ToolState {
662 id: incoming_id.map_or_else(
663 || synthesized_tool_id(&self.attempt_ref, ordinal),
664 str::to_owned,
665 ),
666 ordinal,
667 };
668 self.tool_states.insert(index, state.clone());
669 state
670 };
671 let function = call.get("function").and_then(Value::as_object);
672 let name = function
673 .and_then(|function| function.get("name"))
674 .and_then(Value::as_str)
675 .map(str::to_owned);
676 let arguments = function
677 .and_then(|function| function.get("arguments"))
678 .and_then(Value::as_str)
679 .map(str::to_owned);
680 if name.is_some() || arguments.is_some() || incoming_id.is_some() {
681 let event = OpenRouterStreamEvent::ToolCallDelta {
682 id: state.id,
683 ordinal: state.ordinal,
684 index,
685 name,
686 arguments,
687 };
688 self.replay_guard.observe(&event);
689 events.push(event);
690 }
691 }
692 Ok(())
693 }
694
695 fn error(&self, kind: OpenRouterStreamErrorKind) -> OpenRouterStreamError {
696 OpenRouterStreamError {
697 kind,
698 byte_offset: self.byte_offset,
699 }
700 }
701}
702
703fn required_u64(
704 object: &Map<String, Value>,
705 name: &str,
706 byte_offset: u64,
707) -> Result<u64, OpenRouterStreamError> {
708 object
709 .get(name)
710 .and_then(Value::as_u64)
711 .ok_or(OpenRouterStreamError {
712 kind: OpenRouterStreamErrorKind::InvalidFrame,
713 byte_offset,
714 })
715}
716
717fn synthesized_tool_id(attempt_ref: &str, ordinal: u64) -> String {
718 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
719 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
720 let mut value = FNV_OFFSET;
721 for byte in attempt_ref
722 .bytes()
723 .chain(*b":")
724 .chain(ordinal.to_be_bytes())
725 {
726 value ^= u64::from(byte);
727 value = value.wrapping_mul(FNV_PRIME);
728 }
729 format!("call_{value:016x}")
730}
731
732#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
733pub struct OpenRouterReplayGuard {
734 boundary: Option<ReplayBoundary>,
735 body_written: bool,
736}
737
738impl OpenRouterReplayGuard {
739 pub fn mark_body_written(&mut self) {
740 self.body_written = true;
741 }
742
743 pub fn observe(&mut self, event: &OpenRouterStreamEvent) {
744 match event {
745 OpenRouterStreamEvent::ContentDelta { text } if !text.is_empty() => {
746 self.cross(ReplayBoundary::PublicContent);
747 }
748 OpenRouterStreamEvent::ToolCallDelta { .. } => {
749 self.cross(ReplayBoundary::VisibleToolCall);
750 }
751 OpenRouterStreamEvent::ContentDelta { .. }
752 | OpenRouterStreamEvent::FinishReason { .. }
753 | OpenRouterStreamEvent::Usage(_)
754 | OpenRouterStreamEvent::Done => {}
755 }
756 }
757
758 pub fn mark_forwarded_tool_result(&mut self) {
759 self.cross(ReplayBoundary::ForwardedToolResult);
760 }
761
762 pub fn mark_transport_outcome_unknown(&mut self) {
763 if self.body_written {
764 self.cross(ReplayBoundary::AmbiguousDispatch);
765 }
766 }
767
768 pub fn decision(&self) -> ReplayDecision {
769 self.boundary
770 .map_or(ReplayDecision::Allowed, ReplayDecision::Denied)
771 }
772
773 fn cross(&mut self, boundary: ReplayBoundary) {
774 if self.boundary.is_none() {
775 self.boundary = Some(boundary);
776 }
777 }
778}
779
780#[derive(Clone, Copy, Debug, PartialEq, Eq)]
781pub enum OpenRouterFailureClass {
782 InvalidRequest,
783 Authentication,
784 PaymentRequired,
785 Authorization,
786 NotFound,
787 Timeout,
788 Unprocessable,
789 RateLimited,
790 UpstreamServer,
791 UnexpectedStatus,
792 MismatchedContentType,
793 RedirectRejected,
794 Cancelled,
795 TransportTimeout,
796 AmbiguousDispatch,
797}
798
799#[derive(Clone, Copy, Debug, PartialEq, Eq)]
800pub struct OpenRouterFailure {
801 pub class: OpenRouterFailureClass,
802 pub status: Option<u16>,
803 pub retry_after: Option<BoundedRetryAfter>,
804}
805
806pub const AUTHORIZED_RATE_LIMIT_RETRY_AFTER_MAX_SECONDS: u32 = 2;
810
811#[derive(Clone, Copy, Debug, PartialEq, Eq)]
812pub enum OpenRouterRetryDecision {
813 RetryOnce { retry_after_seconds: u32 },
816 NoRetry,
819}
820
821pub fn authorized_rate_limit_retry(
826 failure: OpenRouterFailure,
827 replay: ReplayDecision,
828 retry_already_used: bool,
829) -> OpenRouterRetryDecision {
830 let RetryDecisionInputs {
831 retry_after_seconds,
832 } = RetryDecisionInputs::from(failure);
833 if retry_already_used
834 || !matches!(failure.class, OpenRouterFailureClass::RateLimited)
835 || !matches!(replay, ReplayDecision::Allowed)
836 || retry_after_seconds
837 .is_none_or(|seconds| seconds > AUTHORIZED_RATE_LIMIT_RETRY_AFTER_MAX_SECONDS)
838 {
839 return OpenRouterRetryDecision::NoRetry;
840 }
841 match retry_after_seconds {
842 Some(retry_after_seconds) => OpenRouterRetryDecision::RetryOnce {
843 retry_after_seconds,
844 },
845 None => OpenRouterRetryDecision::NoRetry,
846 }
847}
848
849struct RetryDecisionInputs {
850 retry_after_seconds: Option<u32>,
851}
852
853impl From<OpenRouterFailure> for RetryDecisionInputs {
854 fn from(failure: OpenRouterFailure) -> Self {
855 Self {
856 retry_after_seconds: failure
857 .retry_after
858 .filter(|retry_after| !retry_after.capped)
859 .map(|retry_after| retry_after.seconds),
860 }
861 }
862}
863
864pub fn normalize_http_response(
865 status: u16,
866 content_type: Option<&str>,
867 retry_after: Option<&str>,
868 redirected: bool,
869) -> Result<(), OpenRouterFailure> {
870 if redirected || (300..400).contains(&status) {
871 return Err(failure(
872 OpenRouterFailureClass::RedirectRejected,
873 Some(status),
874 retry_after,
875 ));
876 }
877 if status != 200 {
878 let class = match status {
879 400 => OpenRouterFailureClass::InvalidRequest,
880 401 => OpenRouterFailureClass::Authentication,
881 402 => OpenRouterFailureClass::PaymentRequired,
882 403 => OpenRouterFailureClass::Authorization,
883 404 => OpenRouterFailureClass::NotFound,
884 408 => OpenRouterFailureClass::Timeout,
885 422 => OpenRouterFailureClass::Unprocessable,
886 429 => OpenRouterFailureClass::RateLimited,
887 500..=599 => OpenRouterFailureClass::UpstreamServer,
888 _ => OpenRouterFailureClass::UnexpectedStatus,
889 };
890 return Err(failure(class, Some(status), retry_after));
891 }
892 let media_type = content_type
893 .and_then(|value| value.split(';').next())
894 .map(str::trim);
895 if media_type != Some("text/event-stream") {
896 return Err(failure(
897 OpenRouterFailureClass::MismatchedContentType,
898 Some(status),
899 None,
900 ));
901 }
902 Ok(())
903}
904
905#[derive(Clone, Copy, Debug, PartialEq, Eq)]
906pub enum BodyWriteState {
907 NotWritten,
908 Written,
909 Unknown,
910}
911
912#[derive(Clone, Copy, Debug, PartialEq, Eq)]
913pub enum TransportFailureSignal {
914 Cancelled,
915 Timeout,
916}
917
918pub fn normalize_transport_failure(
919 signal: TransportFailureSignal,
920 body_write: BodyWriteState,
921) -> OpenRouterFailure {
922 let class = match body_write {
923 BodyWriteState::Written | BodyWriteState::Unknown => {
924 OpenRouterFailureClass::AmbiguousDispatch
925 }
926 BodyWriteState::NotWritten => match signal {
927 TransportFailureSignal::Cancelled => OpenRouterFailureClass::Cancelled,
928 TransportFailureSignal::Timeout => OpenRouterFailureClass::TransportTimeout,
929 },
930 };
931 failure(class, None, None)
932}
933
934fn failure(
935 class: OpenRouterFailureClass,
936 status: Option<u16>,
937 retry_after: Option<&str>,
938) -> OpenRouterFailure {
939 OpenRouterFailure {
940 class,
941 status,
942 retry_after: BoundedRetryAfter::parse(retry_after),
943 }
944}
945
946#[cfg(test)]
947mod tests {
948 use super::*;
949 use router_protocol::{
950 BillingAuthorityClass, CapabilityRequirements, CatalogSourceKind, CredentialSourceKind,
951 EndpointIdentity, ExecutionLocation, FunctionTool, HarnessInstanceId, M1_WIRE_VERSION,
952 ModelNamespace, PhasePlacement, ProviderService, ReasoningIntent, SessionId, TurnId,
953 };
954
955 fn route() -> RouteIdentity {
956 RouteIdentity {
957 phase: PhaseKind::OpenrouterApi,
958 placement: PhasePlacement::Builtin,
959 provider: ProviderService::Openrouter,
960 transport: TransportKind::DirectHttps,
961 endpoint: EndpointIdentity {
962 origin: "https://openrouter.ai".to_owned(),
963 path: "/api/v1/chat/completions".to_owned(),
964 },
965 credential_source: CredentialSourceKind::SecretRef,
966 credential_ref: Some("keychain:openrouter-secret-canary".to_owned()),
967 credential_refs: vec!["keychain:openrouter-secret-canary".to_owned()],
968 catalog_source: CatalogSourceKind::OpenrouterCatalog,
969 catalog_ref: "catalog:openrouter:verified".to_owned(),
970 billing_authority: BillingAuthorityClass::OpenrouterMeteredApi,
971 model_namespace: ModelNamespace::Openrouter,
972 model_id: "anthropic/claude-sonnet-4.5".to_owned(),
973 execution_location: ExecutionLocation::RemoteCloud,
974 account_ref: "account:openrouter".to_owned(),
975 capacity_domain_ref: "capacity:openrouter".to_owned(),
976 local_execution_proof: false,
977 approved_local_inventory: Vec::new(),
978 reasoning_modes: Vec::new(),
979 }
980 }
981
982 fn request(prompt: &str) -> CanonicalRequest {
983 CanonicalRequest {
984 wire_version: M1_WIRE_VERSION.to_owned(),
985 model: "router-auto".to_owned(),
986 stream: true,
987 harness_instance: HarnessInstanceId::parse("fixture-cli").expect("fixture"),
988 session_id: SessionId::parse("session-openrouter").expect("fixture"),
989 turn_id: TurnId::parse("turn-openrouter").expect("fixture"),
990 input: vec![InputItem::Message {
991 role: MessageRole::User,
992 content: vec![ContentBlock::Text {
993 text: prompt.to_owned(),
994 }],
995 }],
996 tools: vec![FunctionTool {
997 namespace: Some("weather".to_owned()),
998 name: "lookup".to_owned(),
999 description: "lookup weather".to_owned(),
1000 parameters: json!({"type":"object"}),
1001 strict: true,
1002 }],
1003 tool_choice: ToolChoice::Auto,
1004 structured_output: None,
1005 reasoning: ReasoningIntent::None,
1006 max_output_tokens: Some(64),
1007 max_spend_micro_usd: None,
1008 capabilities: CapabilityRequirements::default(),
1009 }
1010 }
1011
1012 #[test]
1013 fn exact_request_has_fixed_provider_policy_and_redacted_secret_and_prompt() {
1014 let spec = build_request(&route(), &request("prompt-canary")).expect("request");
1015 assert_eq!(spec.method, HttpMethod::Post);
1016 assert_eq!(spec.origin, "https://openrouter.ai");
1017 assert_eq!(spec.path, "/api/v1/chat/completions");
1018 assert_eq!(spec.accept, "text/event-stream");
1019 assert!(spec.network.require_tls);
1020 assert!(!spec.network.follow_redirects);
1021 assert_eq!(
1022 spec.body(),
1023 &json!({
1024 "model":"anthropic/claude-sonnet-4.5",
1025 "messages":[{"role":"user","content":"prompt-canary"}],
1026 "stream":true,
1027 "stream_options":{"include_usage":true},
1028 "provider":{
1029 "require_parameters":true,
1030 "allow_fallbacks":false,
1031 "data_collection":"deny",
1032 "zdr":true
1033 },
1034 "tool_choice":"auto",
1035 "tools":[{"type":"function","function":{
1036 "name":"weather__lookup",
1037 "description":"lookup weather",
1038 "parameters":{"type":"object"},
1039 "strict":true
1040 }}],
1041 "max_tokens":64
1042 })
1043 );
1044 assert_eq!(
1045 spec.bearer_secret.as_str(),
1046 "keychain:openrouter-secret-canary"
1047 );
1048 let first_payload = spec.body_bytes().expect("serialize exact payload");
1049 let second_payload = spec.body_bytes().expect("serialize exact payload again");
1050 assert_eq!(first_payload, second_payload);
1051 assert!(
1052 !first_payload
1053 .windows("openrouter-secret-canary".len())
1054 .any(|window| window == b"openrouter-secret-canary")
1055 );
1056 let debug = format!("{spec:?}");
1057 assert!(!debug.contains("openrouter-secret-canary"));
1058 assert!(!debug.contains("prompt-canary"));
1059 }
1060
1061 #[test]
1062 fn public_summary_reasoning_keeps_strict_provider_policy_without_reasoning_param() {
1063 let mut request = request("prompt-canary");
1064 request.reasoning = ReasoningIntent::PublicSummary;
1065 let spec = build_request(&route(), &request).expect("request");
1066 let body = spec.body().as_object().expect("body object");
1067 assert!(body.contains_key("provider"));
1068 assert_eq!(
1069 body.get("provider"),
1070 Some(&json!({
1071 "require_parameters": true,
1072 "allow_fallbacks": false,
1073 "data_collection": "deny",
1074 "zdr": true,
1075 }))
1076 );
1077 assert!(
1078 !body.contains_key("reasoning"),
1079 "strict OpenRouter provider routing rejects a reasoning parameter for flash models"
1080 );
1081 }
1082
1083 #[test]
1084 fn sse_is_every_byte_fragment_safe_with_crlf_comments_multiline_and_usage_once() {
1085 let transcript = concat!(
1086 ": heartbeat\r\n\r\n",
1087 "data: {\"choices\":\r\n",
1088 "data: [{\"delta\":{\"content\":\"hé\"},\"finish_reason\":null}]}\r\n\r\n",
1089 "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call-provider\",\"function\":{\"name\":\"weather__lookup\",\"arguments\":\"{\"}}]}}]}\r\n\r\n",
1090 "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"city\\\":\\\"Paris\\\"}\"}}]},\"finish_reason\":\"tool_calls\"}]}\r\n\r\n",
1091 "data: {\"choices\":[],\"usage\":{\"prompt_tokens\":11,\"completion_tokens\":7,\"total_tokens\":18}}\r\n\r\n",
1092 "data: [DONE]"
1093 );
1094 let attempt = AttemptId::parse("attempt-openrouter").expect("attempt");
1095 let mut parser = OpenRouterSseParser::new(&attempt);
1096 let mut fragmented = Vec::new();
1097 for byte in transcript.as_bytes() {
1098 fragmented.extend(parser.feed(std::slice::from_ref(byte)).expect("fragment"));
1099 }
1100 fragmented.extend(parser.finish().expect("complete"));
1101 assert!(matches!(
1102 &fragmented[0],
1103 OpenRouterStreamEvent::ContentDelta { text } if text == "hé"
1104 ));
1105 assert_eq!(
1106 fragmented
1107 .iter()
1108 .filter(|event| matches!(event, OpenRouterStreamEvent::Usage(_)))
1109 .count(),
1110 1
1111 );
1112 assert!(matches!(
1113 fragmented.last(),
1114 Some(OpenRouterStreamEvent::Done)
1115 ));
1116
1117 let mut whole = OpenRouterSseParser::new(&attempt);
1118 let mut whole_events = whole.feed(transcript.as_bytes()).expect("whole stream");
1119 whole_events.extend(whole.finish().expect("complete"));
1120 assert_eq!(fragmented, whole_events);
1121 }
1122
1123 #[test]
1124 fn post_visible_provider_error_denies_replay_without_echoing_canary() {
1125 let attempt = AttemptId::parse("attempt-error").expect("attempt");
1126 let mut parser = OpenRouterSseParser::new(&attempt);
1127 parser.mark_body_written();
1128 let error = parser
1129 .feed(
1130 concat!(
1131 "data: {\"choices\":[{\"delta\":{\"content\":\"public\"}}]}\n\n",
1132 "data: {\"error\":{\"message\":\"secret-canary\"}}\n\n"
1133 )
1134 .as_bytes(),
1135 )
1136 .expect_err("provider error");
1137 assert_eq!(error.kind, OpenRouterStreamErrorKind::ProviderError);
1138 assert_eq!(
1139 parser.replay_decision(),
1140 ReplayDecision::Denied(ReplayBoundary::PublicContent)
1141 );
1142 assert!(!format!("{error:?}").contains("secret-canary"));
1143 }
1144
1145 #[test]
1146 fn usage_and_terminal_are_each_exactly_once() {
1147 let attempt = AttemptId::parse("attempt-terminal").expect("attempt");
1148 let usage = b"data: {\"choices\":[],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":2,\"total_tokens\":3}}\n\n";
1149 let mut duplicate_usage = OpenRouterSseParser::new(&attempt);
1150 duplicate_usage.feed(usage).expect("first usage");
1151 assert_eq!(
1152 duplicate_usage
1153 .feed(usage)
1154 .expect_err("duplicate usage")
1155 .kind,
1156 OpenRouterStreamErrorKind::DuplicateUsage
1157 );
1158
1159 let mut missing_usage = OpenRouterSseParser::new(&attempt);
1160 assert_eq!(
1161 missing_usage
1162 .feed(b"data: [DONE]\n\n")
1163 .expect_err("usage required")
1164 .kind,
1165 OpenRouterStreamErrorKind::MissingUsage
1166 );
1167
1168 let mut duplicate_terminal = OpenRouterSseParser::new(&attempt);
1169 duplicate_terminal.feed(usage).expect("usage");
1170 duplicate_terminal
1171 .feed(b"data: [DONE]\n\n")
1172 .expect("terminal");
1173 assert_eq!(
1174 duplicate_terminal
1175 .feed(b"data: [DONE]\n\n")
1176 .expect_err("duplicate terminal")
1177 .kind,
1178 OpenRouterStreamErrorKind::DuplicateTerminal
1179 );
1180 }
1181
1182 #[test]
1183 fn malformed_oversized_missing_terminal_and_finish_error_fail_closed() {
1184 let attempt = AttemptId::parse("attempt-invalid").expect("attempt");
1185 let cases: &[(&[u8], OpenRouterStreamErrorKind)] = &[
1186 (b"data: {bad}\n\n", OpenRouterStreamErrorKind::InvalidJson),
1187 (b"data: \xff\n\n", OpenRouterStreamErrorKind::InvalidUtf8),
1188 (
1189 b"data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"error\"}]}\n\n",
1190 OpenRouterStreamErrorKind::ProviderError,
1191 ),
1192 ];
1193 for (input, expected) in cases {
1194 let mut parser = OpenRouterSseParser::new(&attempt);
1195 assert_eq!(parser.feed(input).expect_err("invalid").kind, *expected);
1196 }
1197 let mut oversized = OpenRouterSseParser::with_max_event_bytes(&attempt, 8);
1198 assert_eq!(
1199 oversized
1200 .feed(b"data: 123456789")
1201 .expect_err("bounded")
1202 .kind,
1203 OpenRouterStreamErrorKind::EventTooLarge
1204 );
1205 let mut missing = OpenRouterSseParser::new(&attempt);
1206 missing
1207 .feed(b"data: {\"choices\":[]}\n\n")
1208 .expect("valid chunk");
1209 assert_eq!(
1210 missing.finish().expect_err("terminal required").kind,
1211 OpenRouterStreamErrorKind::MissingTerminal
1212 );
1213 }
1214
1215 #[test]
1216 fn status_and_transport_failures_include_402_retry_bounds_and_ambiguity() {
1217 let matrix = [
1218 (400, OpenRouterFailureClass::InvalidRequest),
1219 (401, OpenRouterFailureClass::Authentication),
1220 (402, OpenRouterFailureClass::PaymentRequired),
1221 (403, OpenRouterFailureClass::Authorization),
1222 (404, OpenRouterFailureClass::NotFound),
1223 (408, OpenRouterFailureClass::Timeout),
1224 (422, OpenRouterFailureClass::Unprocessable),
1225 (429, OpenRouterFailureClass::RateLimited),
1226 (503, OpenRouterFailureClass::UpstreamServer),
1227 ];
1228 for (status, class) in matrix {
1229 let failure =
1230 normalize_http_response(status, None, Some("999999"), false).expect_err("failure");
1231 assert_eq!(failure.class, class);
1232 assert!(failure.retry_after.is_some_and(|retry| retry.capped));
1233 }
1234 assert!(
1235 normalize_http_response(200, Some("text/event-stream; charset=utf-8"), None, false)
1236 .is_ok()
1237 );
1238 assert_eq!(
1239 normalize_http_response(200, Some("application/json"), None, false)
1240 .expect_err("content type")
1241 .class,
1242 OpenRouterFailureClass::MismatchedContentType
1243 );
1244 assert_eq!(
1245 normalize_http_response(302, None, None, true)
1246 .expect_err("redirect")
1247 .class,
1248 OpenRouterFailureClass::RedirectRejected
1249 );
1250 assert_eq!(
1251 normalize_transport_failure(TransportFailureSignal::Timeout, BodyWriteState::Written,)
1252 .class,
1253 OpenRouterFailureClass::AmbiguousDispatch
1254 );
1255 let mut guard = OpenRouterReplayGuard::default();
1256 guard.mark_body_written();
1257 guard.mark_transport_outcome_unknown();
1258 assert_eq!(
1259 guard.decision(),
1260 ReplayDecision::Denied(ReplayBoundary::AmbiguousDispatch)
1261 );
1262 }
1263
1264 #[test]
1265 fn rate_limit_retry_requires_one_short_explicit_delay_before_visibility() {
1266 let rate_limited =
1267 normalize_http_response(429, None, Some("2"), false).expect_err("rate limited");
1268 assert_eq!(
1269 authorized_rate_limit_retry(rate_limited, ReplayDecision::Allowed, false),
1270 OpenRouterRetryDecision::RetryOnce {
1271 retry_after_seconds: 2
1272 }
1273 );
1274
1275 for (retry_after, replay, already_used) in [
1276 (None, ReplayDecision::Allowed, false),
1277 (Some("3"), ReplayDecision::Allowed, false),
1278 (Some("2"), ReplayDecision::Allowed, true),
1279 (
1280 Some("2"),
1281 ReplayDecision::Denied(ReplayBoundary::AmbiguousDispatch),
1282 false,
1283 ),
1284 (
1285 Some("2"),
1286 ReplayDecision::Denied(ReplayBoundary::PublicContent),
1287 false,
1288 ),
1289 (
1290 Some("2"),
1291 ReplayDecision::Denied(ReplayBoundary::VisibleToolCall),
1292 false,
1293 ),
1294 (
1295 Some("2"),
1296 ReplayDecision::Denied(ReplayBoundary::ForwardedToolResult),
1297 false,
1298 ),
1299 ] {
1300 let failure =
1301 normalize_http_response(429, None, retry_after, false).expect_err("rate limited");
1302 assert_eq!(
1303 authorized_rate_limit_retry(failure, replay, already_used),
1304 OpenRouterRetryDecision::NoRetry
1305 );
1306 }
1307 }
1308}