Skip to content

Commit a6510da

Browse files
authored
feat(openrouter): Include raw API response context in structured decoding exceptions. (#824)
1 parent e890954 commit a6510da

File tree

3 files changed

+81
-1
lines changed

3 files changed

+81
-1
lines changed

src/Providers/OpenRouter/Handlers/Structured.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Illuminate\Http\Client\PendingRequest;
88
use Prism\Prism\Exceptions\PrismException;
9+
use Prism\Prism\Exceptions\PrismStructuredDecodingException;
910
use Prism\Prism\Providers\OpenRouter\Concerns\BuildsRequestOptions;
1011
use Prism\Prism\Providers\OpenRouter\Concerns\MapsFinishReason;
1112
use Prism\Prism\Providers\OpenRouter\Concerns\ValidatesResponses;
@@ -109,6 +110,17 @@ protected function createResponse(Request $request, array $data): StructuredResp
109110

110111
$this->responseBuilder->addStep($step);
111112

112-
return $this->responseBuilder->toResponse();
113+
try {
114+
return $this->responseBuilder->toResponse();
115+
} catch (PrismStructuredDecodingException $e) {
116+
$context = sprintf(
117+
"\nModel: %s\nFinish reason: %s\nRaw choices: %s",
118+
data_get($data, 'model', 'unknown'),
119+
data_get($data, 'choices.0.finish_reason', 'unknown'),
120+
json_encode(data_get($data, 'choices'), JSON_PRETTY_PRINT)
121+
);
122+
123+
throw new PrismStructuredDecodingException($e->getMessage().$context);
124+
}
113125
}
114126
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"id": "gen-structured-empty",
3+
"model": "anthropic/claude-3.5-sonnet",
4+
"choices": [
5+
{
6+
"message": {
7+
"role": "assistant",
8+
"content": null
9+
},
10+
"finish_reason": "stop"
11+
}
12+
],
13+
"usage": {
14+
"prompt_tokens": 100,
15+
"completion_tokens": 0
16+
}
17+
}

tests/Providers/OpenRouter/StructuredTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Http\Client\Request;
66
use Illuminate\Support\Facades\Http;
77
use Prism\Prism\Enums\Provider;
8+
use Prism\Prism\Exceptions\PrismStructuredDecodingException;
89
use Prism\Prism\Facades\Prism;
910
use Prism\Prism\Schema\BooleanSchema;
1011
use Prism\Prism\Schema\ObjectSchema;
@@ -154,3 +155,53 @@
154155
&& $payload['verbosity'] === 'medium';
155156
});
156157
});
158+
159+
it('throws enriched exception when content is empty', function (): void {
160+
FixtureResponse::fakeResponseSequence('v1/chat/completions', 'openrouter/structured-empty-content');
161+
162+
$schema = new ObjectSchema(
163+
'output',
164+
'the output object',
165+
[
166+
new StringSchema('data', 'Some data'),
167+
],
168+
['data']
169+
);
170+
171+
expect(fn () => Prism::structured()
172+
->withSchema($schema)
173+
->using(Provider::OpenRouter, 'anthropic/claude-3.5-sonnet')
174+
->withPrompt('Give me some data')
175+
->asStructured()
176+
)->toThrow(PrismStructuredDecodingException::class);
177+
});
178+
179+
it('includes raw response context in decoding exception', function (): void {
180+
FixtureResponse::fakeResponseSequence('v1/chat/completions', 'openrouter/structured-empty-content');
181+
182+
$schema = new ObjectSchema(
183+
'output',
184+
'the output object',
185+
[
186+
new StringSchema('data', 'Some data'),
187+
],
188+
['data']
189+
);
190+
191+
try {
192+
Prism::structured()
193+
->withSchema($schema)
194+
->using(Provider::OpenRouter, 'anthropic/claude-3.5-sonnet')
195+
->withPrompt('Give me some data')
196+
->asStructured();
197+
198+
$this->fail('Expected PrismStructuredDecodingException to be thrown');
199+
} catch (PrismStructuredDecodingException $e) {
200+
expect($e->getMessage())
201+
->toContain('Structured object could not be decoded')
202+
->toContain('Model: anthropic/claude-3.5-sonnet')
203+
->toContain('Finish reason: stop')
204+
->toContain('Raw choices:')
205+
->toContain('"content": null');
206+
}
207+
});

0 commit comments

Comments
 (0)