Repository: tmc/langchaingo (2025-08-08T16:51:43Z)

Issue:
openai 调用出错了
这个是错误消息
error decoding streaming response: invalid character ':' looking for beginning of value
经过排查文件是在llms/openai/internal/openaiclient/chat.go的parseStreamingChatResponse方法，在协程里解析响应数据出错了


Discussion:
经过排查，比如使用 openrouter 那么第一条消息是:openrouter 如果是其他的平台，可能前缀不一样，这里没有兼容这种情况，希望可以处理一下
Is this a duplicate of #942 ?
应该是同一个类型的问题
![Image](https://github.com/user-attachments/assets/0769e2ab-7d3b-4cea-aeb7-0f1ff24b7736)

通过打印输出，可以看到，确实是这里的问题
这个问题解决的还是有点片面，只兼容了一种情况
https://github.com/tmc/langchaingo/issues/942

针对这个问题 我提了一个 PR 。
https://github.com/tmc/langchaingo/pull/1173



Patch:
diff --git a/examples/openrouter-llm-example/README.md b/examples/openrouter-llm-example/README.md
new file mode 100644
index 000000000..b7af2f5e7
--- /dev/null
+++ b/examples/openrouter-llm-example/README.md
@@ -0,0 +1,51 @@
+# OpenRouter LLM Example
+
+This example demonstrates how to use OpenRouter with langchaingo. OpenRouter provides a unified API to access various LLM models through a single endpoint.
+
+## About OpenRouter
+
+OpenRouter is an AI routing service that provides:
+- Access to multiple LLM providers (OpenAI, Anthropic, Google, Meta, etc.) through a single API
+- Automatic failover and load balancing
+- Usage tracking and analytics
+- Support for both free and premium models
+
+## Setup
+
+1. Get an OpenRouter API key from https://openrouter.ai/
+2. Set the environment variable:
+   ```bash
+   export OPENROUTER_API_KEY="your-api-key-here"
+   ```
+
+## Usage
+
+OpenRouter uses an OpenAI-compatible API, so you can use the OpenAI client with a custom base URL:
+
+```go
+llm, err := openai.New(
+    openai.WithModel("meta-llama/llama-3.2-3b-instruct:free"),
+    openai.WithBaseURL("https://openrouter.ai/api/v1"),
+    openai.WithToken(apiKey),
+)
+```
+
+## Available Models
+
+OpenRouter provides access to many models including:
+- **Free tier**: `meta-llama/llama-3.2-3b-instruct:free`, `google/gemma-2-9b-it:free`
+- **Premium**: GPT-4, Claude, Gemini Pro, and many more
+- Check https://openrouter.ai/models for the full list
+
+## Running the Example
+
+```bash
+go run openrouter_llm_example.go
+```
+
+## Features
+
+- ✅ Streaming responses supported
+- ✅ Compatible with OpenAI client
+- ✅ Access to multiple model providers
+- ✅ Automatic handling of provider-specific quirks
\ No newline at end of file
diff --git a/examples/openrouter-llm-example/go.mod b/examples/openrouter-llm-example/go.mod
new file mode 100644
index 000000000..39ba643b8
--- /dev/null
+++ b/examples/openrouter-llm-example/go.mod
@@ -0,0 +1,15 @@
+module openrouter-llm-example
+
+go 1.23.0
+
+toolchain go1.24.1
+
+require github.com/tmc/langchaingo v0.1.13
+
+replace github.com/tmc/langchaingo => ../..
+
+require (
+	github.com/dlclark/regexp2 v1.10.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/pkoukk/tiktoken-go v0.1.6 // indirect
+)
diff --git a/examples/openrouter-llm-example/go.sum b/examples/openrouter-llm-example/go.sum
new file mode 100644
index 000000000..52f831b92
--- /dev/null
+++ b/examples/openrouter-llm-example/go.sum
@@ -0,0 +1,22 @@
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
+github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
+github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/examples/openrouter-llm-example/openrouter_llm_example.go b/examples/openrouter-llm-example/openrouter_llm_example.go
new file mode 100644
index 000000000..6f26a4b31
--- /dev/null
+++ b/examples/openrouter-llm-example/openrouter_llm_example.go
@@ -0,0 +1,84 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"strings"
+
+	"github.com/tmc/langchaingo/llms"
+	"github.com/tmc/langchaingo/llms/openai"
+)
+
+func main() {
+	// Command-line flags
+	model := flag.String("model", "meta-llama/llama-3.2-3b-instruct:free", "OpenRouter model to use (see https://openrouter.ai/models)")
+	prompt := flag.String("prompt", "Write a haiku about Go programming language.", "Prompt to send to the model")
+	temperature := flag.Float64("temp", 0.8, "Temperature for response generation (0.0-2.0)")
+	streaming := flag.Bool("stream", true, "Use streaming mode")
+	flag.Parse()
+
+	// OpenRouter provides access to multiple LLM providers through a unified API
+	// Get your API key from https://openrouter.ai/
+	apiKey := os.Getenv("OPENROUTER_API_KEY")
+	if apiKey == "" {
+		log.Fatal("Please set OPENROUTER_API_KEY environment variable\n" +
+			"Get your key from https://openrouter.ai/")
+	}
+
+	// Create an OpenAI-compatible client configured for OpenRouter
+	llm, err := openai.New(
+		openai.WithModel(*model),
+		openai.WithBaseURL("https://openrouter.ai/api/v1"),
+		openai.WithToken(apiKey),
+	)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	ctx := context.Background()
+
+	fmt.Println("🚀 OpenRouter CLI - langchaingo")
+	fmt.Println(strings.Repeat("=", 50))
+	fmt.Printf("Model: %s\n", *model)
+	fmt.Printf("Temperature: %.1f\n", *temperature)
+	fmt.Printf("Streaming: %v\n", *streaming)
+	fmt.Printf("Prompt: %s\n", *prompt)
+	fmt.Println(strings.Repeat("-", 50))
+	fmt.Println()
+
+	// Generate response
+	opts := []llms.CallOption{
+		llms.WithTemperature(*temperature),
+	}
+
+	if *streaming {
+		opts = append(opts, llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
+			fmt.Print(string(chunk))
+			return nil
+		}))
+	}
+
+	response, err := llms.GenerateFromSinglePrompt(ctx, llm, *prompt, opts...)
+
+	if !*streaming && err == nil {
+		fmt.Println(response)
+	}
+
+	fmt.Println()
+
+	if err != nil {
+		if strings.Contains(err.Error(), "429") {
+			fmt.Println("⚠️  Rate limit reached. Free tier models are limited to 1 request per minute.")
+			fmt.Println("    Try using a different model with -model flag")
+		} else {
+			log.Printf("Error: %v\n", err)
+		}
+		os.Exit(1)
+	}
+
+	fmt.Println("✅ Success!")
+}
+
diff --git a/llms/openai/internal/openaiclient/chat.go b/llms/openai/internal/openaiclient/chat.go
index f7bffe4e9..79437c0d2 100644
--- a/llms/openai/internal/openaiclient/chat.go
+++ b/llms/openai/internal/openaiclient/chat.go
@@ -454,9 +454,10 @@ func parseStreamingChatResponse(ctx context.Context, r *http.Response, payload *
 			var streamPayload StreamedChatResponsePayload
 			err := json.NewDecoder(bytes.NewReader([]byte(data))).Decode(&streamPayload)
 			if err != nil {
-				streamPayload.Error = fmt.Errorf("error decoding streaming response: %w", err)
-				responseChan <- streamPayload
-				return
+				// Skip non-JSON lines that some providers (e.g., OpenRouter) send as prefixes
+				// These are typically in the format ":provider" or other non-standard SSE data
+				// This allows the stream to continue processing valid JSON chunks
+				continue
 			}
 			responseChan <- streamPayload
 		}


Test patch:
diff --git a/llms/openai/openrouter_httprr_test.go b/llms/openai/openrouter_httprr_test.go
new file mode 100644
index 000000000..28feb2590
--- /dev/null
+++ b/llms/openai/openrouter_httprr_test.go
@@ -0,0 +1,70 @@
+package openai
+
+import (
+	"context"
+	"net/http"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/tmc/langchaingo/internal/httprr"
+	"github.com/tmc/langchaingo/llms"
+)
+
+// TestOpenRouterWithHTTPRR tests OpenRouter integration with recorded HTTP responses
+func TestOpenRouterWithHTTPRR(t *testing.T) {
+	// This test uses httprr to record/replay OpenRouter API responses
+	// Run with -httprecord=. and OPENROUTER_API_KEY set to record new responses
+
+	httprr.SkipIfNoCredentialsAndRecordingMissing(t, "OPENROUTER_API_KEY")
+
+	rr := httprr.OpenForTest(t, http.DefaultTransport)
+
+	// Only run tests in parallel when not recording
+	if !rr.Recording() {
+		t.Parallel()
+	}
+
+	apiKey := "test-api-key"
+	if key := os.Getenv("OPENROUTER_API_KEY"); key != "" && rr.Recording() {
+		apiKey = key
+	}
+
+	// Create OpenAI client configured for OpenRouter
+	llm, err := New(
+		WithToken(apiKey),
+		WithBaseURL("https://openrouter.ai/api/v1"),
+		WithModel("meta-llama/llama-3.2-3b-instruct:free"),
+		WithHTTPClient(rr.Client()),
+	)
+	require.NoError(t, err)
+
+	ctx := context.Background()
+
+	t.Run("streaming", func(t *testing.T) {
+		var chunks []string
+
+		_, err := llm.Call(ctx, "Reply with exactly 'OK' and nothing else",
+			llms.WithTemperature(0.0),
+			llms.WithMaxTokens(10),
+			llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
+				chunks = append(chunks, string(chunk))
+				return nil
+			}),
+		)
+
+		// Skip test if rate limited during recording
+		if err != nil && strings.Contains(err.Error(), "429") && rr.Recording() {
+			t.Skip("Rate limited during recording - this is expected with free tier")
+		}
+
+		require.NoError(t, err, "streaming should work with OpenRouter")
+		assert.NotEmpty(t, chunks, "should receive streamed chunks")
+
+		// The response should contain OK
+		fullResponse := strings.Join(chunks, "")
+		assert.Contains(t, strings.ToUpper(fullResponse), "OK", "response should contain 'OK'")
+	})
+}
diff --git a/llms/openai/openrouter_streaming_test.go b/llms/openai/openrouter_streaming_test.go
new file mode 100644
index 000000000..1123ad60c
--- /dev/null
+++ b/llms/openai/openrouter_streaming_test.go
@@ -0,0 +1,141 @@
+package openai
+
+import (
+	"context"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/tmc/langchaingo/llms"
+)
+
+// TestOpenRouterStreamingPrefix tests that the client correctly handles OpenRouter's ":openrouter" prefix
+func TestOpenRouterStreamingPrefix(t *testing.T) {
+	t.Parallel()
+
+	// Create a mock server that simulates OpenRouter's response with prefix
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "text/event-stream")
+
+		// Simulate OpenRouter's response with the problematic prefix
+		responses := []string{
+			":openrouter", // This is the prefix that used to cause errors
+			"",
+			"data: {\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1234567890,\"model\":\"test\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"},\"finish_reason\":null}]}",
+			"",
+			"data: {\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1234567890,\"model\":\"test\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Test\"},\"finish_reason\":null}]}",
+			"",
+			"data: {\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1234567890,\"model\":\"test\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" response\"},\"finish_reason\":null}]}",
+			"",
+			"data: {\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1234567890,\"model\":\"test\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}",
+			"",
+			"data: [DONE]",
+		}
+
+		flusher, ok := w.(http.Flusher)
+		if !ok {
+			http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
+			return
+		}
+
+		for _, response := range responses {
+			_, _ = w.Write([]byte(response + "\n"))
+			flusher.Flush()
+			time.Sleep(10 * time.Millisecond)
+		}
+	}))
+	defer server.Close()
+
+	// Create client pointing to mock server
+	llm, err := New(
+		WithToken("test-key"),
+		WithBaseURL(server.URL),
+		WithModel("test-model"),
+	)
+	require.NoError(t, err)
+
+	ctx := context.Background()
+	var chunks []string
+
+	_, err = llm.Call(ctx, "Test message",
+		llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
+			chunks = append(chunks, string(chunk))
+			return nil
+		}),
+	)
+
+	// The fix ensures this doesn't fail despite the ":openrouter" prefix
+	require.NoError(t, err, "should handle OpenRouter prefix without error")
+	assert.NotEmpty(t, chunks, "should receive streamed chunks")
+
+	fullResponse := strings.Join(chunks, "")
+	assert.Equal(t, "Test response", fullResponse, "should receive complete response")
+}
+
+// TestOpenRouterRateLimitHandling tests graceful handling of 429 rate limit errors
+func TestOpenRouterRateLimitHandling(t *testing.T) {
+	t.Parallel()
+
+	requestCount := 0
+
+	// Create a mock server that returns 429 on first request, then succeeds
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		requestCount++
+
+		if requestCount == 1 {
+			// First request returns rate limit error
+			w.Header().Set("Content-Type", "application/json")
+			w.WriteHeader(http.StatusTooManyRequests)
+			_, _ = w.Write([]byte(`{
+				"error": {
+					"message": "Rate limit exceeded: limit_rpm/meta-llama/llama-3.2-3b-instruct. Limited to 1 request per minute.",
+					"type": "rate_limit_error",
+					"code": "rate_limit_exceeded"
+				}
+			}`))
+			return
+		}
+
+		// Second request succeeds
+		w.Header().Set("Content-Type", "application/json")
+		_, _ = w.Write([]byte(`{
+			"id": "chatcmpl-123",
+			"object": "chat.completion",
+			"created": 1234567890,
+			"model": "test",
+			"choices": [{
+				"index": 0,
+				"message": {
+					"role": "assistant",
+					"content": "Success after retry"
+				},
+				"finish_reason": "stop"
+			}]
+		}`))
+	}))
+	defer server.Close()
+
+	// Create client pointing to mock server
+	llm, err := New(
+		WithToken("test-key"),
+		WithBaseURL(server.URL),
+		WithModel("test-model"),
+	)
+	require.NoError(t, err)
+
+	ctx := context.Background()
+
+	// First call should fail with rate limit
+	_, err = llm.Call(ctx, "Test message")
+	assert.Error(t, err, "should return rate limit error")
+	assert.Contains(t, err.Error(), "429", "error should indicate rate limit")
+
+	// Second call should succeed (simulating retry after rate limit)
+	response, err := llm.Call(ctx, "Test message")
+	require.NoError(t, err, "should succeed after rate limit clears")
+	assert.Equal(t, "Success after retry", response)
+}
diff --git a/llms/openai/testdata/TestOpenRouterStreaming.httprr b/llms/openai/testdata/TestOpenRouterStreaming.httprr
new file mode 100644
index 000000000..27f272003
--- /dev/null
+++ b/llms/openai/testdata/TestOpenRouterStreaming.httprr
@@ -0,0 +1,58 @@
+httprr trace v1
+410 2302
+POST https://openrouter.ai/api/v1/chat/completions HTTP/1.1
+Host: openrouter.ai
+User-Agent: langchaingo-httprr
+Content-Length: 205
+Authorization: Bearer test-api-key
+Content-Type: application/json
+
+{"model":"meta-llama/llama-3.2-3b-instruct:free","messages":[{"role":"user","content":"Say exactly 'test response' and nothing else"}],"temperature":0,"stream":true,"stream_options":{"include_usage":true}}HTTP/2.0 200 OK
+Content-Length: 1820
+Access-Control-Allow-Origin: *
+Cache-Control: no-cache
+Content-Type: text/event-stream
+Date: Fri, 08 Aug 2025 15:40:32 GMT
+Permissions-Policy: payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com")
+Referrer-Policy: no-referrer, strict-origin-when-cross-origin
+Server: cloudflare
+Vary: Accept-Encoding
+X-Content-Type-Options: nosniff
+
+: OPENROUTER PROCESSING
+
+data: {"id":"gen-1754667632-NNYO7FUAFP6cwNW8jL7x","provider":"Venice","model":"meta-llama/llama-3.2-3b-instruct:free","object":"chat.completion.chunk","created":1754667632,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+data: {"id":"gen-1754667632-NNYO7FUAFP6cwNW8jL7x","provider":"Venice","model":"meta-llama/llama-3.2-3b-instruct:free","object":"chat.completion.chunk","created":1754667632,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+data: {"id":"gen-1754667632-NNYO7FUAFP6cwNW8jL7x","provider":"Venice","model":"meta-llama/llama-3.2-3b-instruct:free","object":"chat.completion.chunk","created":1754667632,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+data: {"id":"gen-1754667632-NNYO7FUAFP6cwNW8jL7x","provider":"Venice","model":"meta-llama/llama-3.2-3b-instruct:free","object":"chat.completion.chunk","created":1754667632,"choices":[{"index":0,"delta":{"role":"assistant","content":"test response"},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
+
+data: {"id":"gen-1754667632-NNYO7FUAFP6cwNW8jL7x","provider":"Venice","model":"meta-llama/llama-3.2-3b-instruct:free","object":"chat.completion.chunk","created":1754667632,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":586,"completion_tokens":3,"total_tokens":589,"cost":0,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0},"cost_details":{"upstream_inference_cost":null},"completion_tokens_details":{"reasoning_tokens":0}}}
+
+data: [DONE]
+
+356 993
+POST https://openrouter.ai/api/v1/chat/completions HTTP/1.1
+Host: openrouter.ai
+User-Agent: langchaingo-httprr
+Content-Length: 151
+Authorization: Bearer test-api-key
+Content-Type: application/json
+
+{"model":"meta-llama/llama-3.2-3b-instruct:free","messages":[{"role":"user","content":"Say exactly 'test response' and nothing else"}],"temperature":0}HTTP/2.0 429 Too Many Requests
+Content-Length: 441
+Access-Control-Allow-Origin: *
+Content-Type: application/json
+Date: Fri, 08 Aug 2025 15:40:34 GMT
+Permissions-Policy: payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com")
+Referrer-Policy: no-referrer, strict-origin-when-cross-origin
+Server: cloudflare
+Vary: Accept-Encoding
+X-Content-Type-Options: nosniff
+X-Ratelimit-Limit: 1
+X-Ratelimit-Remaining: 0
+X-Ratelimit-Reset: 1754667660000
+
+{"error":{"message":"Rate limit exceeded: limit_rpm/meta-llama/llama-3.2-3b-instruct/e8440b11-29fb-4887-a222-eff9ba33dfbf. High demand for meta-llama/llama-3.2-3b-instruct:free on OpenRouter - limited to 1 requests per minute. Please retry shortly.","code":429,"metadata":{"headers":{"X-RateLimit-Limit":"1","X-RateLimit-Remaining":"0","X-RateLimit-Reset":"1754667660000"},"provider_name":null}},"user_id":"user_2g27FbGqkGs5rDR6kxYqEb3sJSR"}
\ No newline at end of file
diff --git a/llms/openai/testdata/TestOpenRouterWithHTTPRR.httprr b/llms/openai/testdata/TestOpenRouterWithHTTPRR.httprr
new file mode 100644
index 000000000..f97359b17
--- /dev/null
+++ b/llms/openai/testdata/TestOpenRouterWithHTTPRR.httprr
@@ -0,0 +1,33 @@
+httprr trace v1
+433 1986
+POST https://openrouter.ai/api/v1/chat/completions HTTP/1.1
+Host: openrouter.ai
+User-Agent: langchaingo-httprr
+Content-Length: 228
+Authorization: Bearer test-api-key
+Content-Type: application/json
+
+{"model":"meta-llama/llama-3.2-3b-instruct:free","messages":[{"role":"user","content":"Reply with exactly 'OK' and nothing else"}],"temperature":0,"max_completion_tokens":10,"stream":true,"stream_options":{"include_usage":true}}HTTP/2.0 200 OK
+Content-Length: 1504
+Access-Control-Allow-Origin: *
+Cache-Control: no-cache
+Content-Type: text/event-stream
+Date: Fri, 08 Aug 2025 16:10:55 GMT
+Permissions-Policy: payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com")
+Referrer-Policy: no-referrer, strict-origin-when-cross-origin
+Server: cloudflare
+Vary: Accept-Encoding
+X-Content-Type-Options: nosniff
+
+: OPENROUTER PROCESSING
+
+data: {"id":"gen-1754669454-aYJhI6K2Oss8Nif9NiYy","provider":"Venice","model":"meta-llama/llama-3.2-3b-instruct:free","object":"chat.completion.chunk","created":1754669454,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+data: {"id":"gen-1754669454-aYJhI6K2Oss8Nif9NiYy","provider":"Venice","model":"meta-llama/llama-3.2-3b-instruct:free","object":"chat.completion.chunk","created":1754669454,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+
+data: {"id":"gen-1754669454-aYJhI6K2Oss8Nif9NiYy","provider":"Venice","model":"meta-llama/llama-3.2-3b-instruct:free","object":"chat.completion.chunk","created":1754669454,"choices":[{"index":0,"delta":{"role":"assistant","content":"OK"},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
+
+data: {"id":"gen-1754669454-aYJhI6K2Oss8Nif9NiYy","provider":"Venice","model":"meta-llama/llama-3.2-3b-instruct:free","object":"chat.completion.chunk","created":1754669454,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":586,"completion_tokens":2,"total_tokens":588,"cost":0,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0},"cost_details":{"upstream_inference_cost":null},"completion_tokens_details":{"reasoning_tokens":0}}}
+
+data: [DONE]
+