Skip to content

Commit cfb62e0

Browse files
authored
refactor: Ollama API Conversion (#157)
1 parent 5c9f80b commit cfb62e0

20 files changed

+273
-271
lines changed

Diff for: config/prism.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
'version' => env('ANTHROPIC_API_VERSION', '2023-06-01'),
1919
],
2020
'ollama' => [
21-
'url' => env('OLLAMA_URL', 'http://localhost:11434/v1'),
21+
'url' => env('OLLAMA_URL', 'http://localhost:11434'),
2222
],
2323
'mistral' => [
2424
'api_key' => env('MISTRAL_API_KEY', ''),

Diff for: src/Providers/Ollama/Handlers/Embeddings.php

+5-9
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,15 @@ public function handle(Request $request): EmbeddingsResponse
2020
{
2121
try {
2222
$response = $this->sendRequest($request);
23+
$data = $response->json();
2324
} catch (Throwable $e) {
2425
throw PrismException::providerRequestError($request->model, $e);
2526
}
2627

27-
$data = $response->json();
28-
2928
if (! $data || data_get($data, 'error')) {
30-
throw PrismException::providerResponseError(vsprintf(
31-
'Ollama Error: [%s] %s',
32-
[
33-
data_get($data, 'error.type', 'unknown'),
34-
data_get($data, 'error.message', 'unknown'),
35-
]
29+
throw PrismException::providerResponseError(sprintf(
30+
'Ollama Error: %s',
31+
data_get($data, 'error', 'unknown'),
3632
));
3733
}
3834

@@ -45,7 +41,7 @@ public function handle(Request $request): EmbeddingsResponse
4541
protected function sendRequest(Request $request): Response
4642
{
4743
return $this->client->post(
48-
'embeddings',
44+
'api/embed',
4945
[
5046
'model' => $request->model,
5147
'input' => $request->input,

Diff for: src/Providers/Ollama/Handlers/Structured.php

+14-34
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use EchoLabs\Prism\Providers\Ollama\Maps\FinishReasonMap;
99
use EchoLabs\Prism\Providers\Ollama\Maps\MessageMap;
1010
use EchoLabs\Prism\Structured\Request;
11-
use EchoLabs\Prism\ValueObjects\Messages\SystemMessage;
1211
use EchoLabs\Prism\ValueObjects\ProviderResponse;
1312
use EchoLabs\Prism\ValueObjects\ResponseMeta;
1413
use EchoLabs\Prism\ValueObjects\Usage;
@@ -23,59 +22,40 @@ public function __construct(protected PendingRequest $client) {}
2322
public function handle(Request $request): ProviderResponse
2423
{
2524
try {
26-
$request = $this->appendMessageForJsonMode($request);
2725
$response = $this->sendRequest($request);
2826
$data = $response->json();
2927
} catch (Throwable $e) {
3028
throw PrismException::providerRequestError($request->model, $e);
3129
}
3230

3331
if (! $data || data_get($data, 'error')) {
34-
throw PrismException::providerResponseError(vsprintf(
35-
'Ollama Error: [%s] %s',
36-
[
37-
data_get($data, 'error.type', 'unknown'),
38-
data_get($data, 'error.message', 'unknown'),
39-
]
32+
throw PrismException::providerResponseError(sprintf(
33+
'Ollama Error: %s',
34+
data_get($data, 'error', 'unknown'),
4035
));
4136
}
4237

4338
return new ProviderResponse(
44-
text: data_get($data, 'choices.0.message.content') ?? '',
39+
text: data_get($data, 'message.content') ?? '',
4540
toolCalls: [],
4641
usage: new Usage(
47-
data_get($data, 'usage.prompt_tokens'),
48-
data_get($data, 'usage.completion_tokens'),
42+
data_get($data, 'prompt_eval_count', 0),
43+
data_get($data, 'eval_count', 0),
4944
),
50-
finishReason: FinishReasonMap::map(data_get($data, 'choices.0.finish_reason', '')),
45+
finishReason: FinishReasonMap::map(data_get($data, 'done_reason', '')),
5146
responseMeta: new ResponseMeta(
52-
id: data_get($data, 'id'),
53-
model: data_get($data, 'model'),
47+
id: '',
48+
model: $request->model,
5449
)
5550
);
5651
}
5752

5853
public function sendRequest(Request $request): Response
5954
{
60-
return $this->client->post(
61-
'chat/completions',
62-
array_merge([
63-
'model' => $request->model,
64-
'messages' => (new MessageMap($request->messages, $request->systemPrompt ?? ''))(),
65-
'max_tokens' => $request->maxTokens ?? 2048,
66-
'format' => ['type' => 'json_object'],
67-
], array_filter([
68-
'temperature' => $request->temperature,
69-
'top_p' => $request->topP,
70-
]))
71-
);
72-
}
73-
74-
protected function appendMessageForJsonMode(Request $request): Request
75-
{
76-
return $request->addMessage(new SystemMessage(sprintf(
77-
"Respond with ONLY JSON that matches the following schema: \n %s",
78-
json_encode($request->schema->toArray(), JSON_PRETTY_PRINT)
79-
)));
55+
return $this->client->post('api/chat', ['model' => $request->model, 'system' => $request->systemPrompt, 'messages' => (new MessageMap($request->messages))->map(), 'format' => $request->schema->toArray(), 'stream' => false, 'options' => array_filter([
56+
'temperature' => $request->temperature,
57+
'num_predict' => $request->maxTokens ?? 2048,
58+
'top_p' => $request->topP,
59+
])]);
8060
}
8161
}

Diff for: src/Providers/Ollama/Handlers/Text.php

+36-26
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace EchoLabs\Prism\Providers\Ollama\Handlers;
66

7+
use EchoLabs\Prism\Enums\FinishReason;
78
use EchoLabs\Prism\Exceptions\PrismException;
89
use EchoLabs\Prism\Providers\Ollama\Maps\FinishReasonMap;
910
use EchoLabs\Prism\Providers\Ollama\Maps\MessageMap;
@@ -25,51 +26,48 @@ public function handle(Request $request): ProviderResponse
2526
{
2627
try {
2728
$response = $this->sendRequest($request);
29+
$data = $response->json();
2830
} catch (Throwable $e) {
2931
throw PrismException::providerRequestError($request->model, $e);
3032
}
3133

32-
$data = $response->json();
33-
3434
if (! $data || data_get($data, 'error')) {
35-
throw PrismException::providerResponseError(vsprintf(
36-
'Ollama Error: [%s] %s',
37-
[
38-
data_get($data, 'error.type', 'unknown'),
39-
data_get($data, 'error.message', 'unknown'),
40-
]
35+
throw PrismException::providerResponseError(sprintf(
36+
'Ollama Error: %s',
37+
data_get($data, 'error', 'unknown'),
4138
));
4239
}
4340

4441
return new ProviderResponse(
45-
text: data_get($data, 'choices.0.message.content') ?? '',
46-
toolCalls: $this->mapToolCalls(data_get($data, 'choices.0.message.tool_calls', []) ?? []),
42+
text: data_get($data, 'message.content') ?? '',
43+
toolCalls: $this->mapToolCalls(data_get($data, 'message.tool_calls', []) ?? []),
4744
usage: new Usage(
48-
data_get($data, 'usage.prompt_tokens'),
49-
data_get($data, 'usage.completion_tokens'),
45+
data_get($data, 'prompt_eval_count', 0),
46+
data_get($data, 'eval_count', 0),
5047
),
51-
finishReason: FinishReasonMap::map(data_get($data, 'choices.0.finish_reason', '')),
48+
finishReason: $this->mapFinishReason($data),
5249
responseMeta: new ResponseMeta(
53-
id: data_get($data, 'id'),
54-
model: data_get($data, 'model'),
50+
id: '',
51+
model: $request->model,
5552
)
5653
);
5754
}
5855

5956
public function sendRequest(Request $request): Response
6057
{
61-
return $this->client->post(
62-
'chat/completions',
63-
array_merge([
58+
return $this
59+
->client
60+
->post('api/chat', [
6461
'model' => $request->model,
65-
'messages' => (new MessageMap($request->messages, $request->systemPrompt ?? ''))(),
66-
'max_tokens' => $request->maxTokens ?? 2048,
67-
], array_filter([
68-
'temperature' => $request->temperature,
69-
'top_p' => $request->topP,
62+
'system' => $request->systemPrompt,
63+
'messages' => (new MessageMap($request->messages))->map(),
7064
'tools' => ToolMap::map($request->tools),
71-
]))
72-
);
65+
'stream' => false,
66+
'options' => array_filter([
67+
'temperature' => $request->temperature,
68+
'num_predict' => $request->maxTokens ?? 2048,
69+
'top_p' => $request->topP,
70+
])]);
7371
}
7472

7573
/**
@@ -79,9 +77,21 @@ public function sendRequest(Request $request): Response
7977
protected function mapToolCalls(array $toolCalls): array
8078
{
8179
return array_map(fn (array $toolCall): ToolCall => new ToolCall(
82-
id: data_get($toolCall, 'id'),
80+
id: data_get($toolCall, 'id', ''),
8381
name: data_get($toolCall, 'function.name'),
8482
arguments: data_get($toolCall, 'function.arguments'),
8583
), $toolCalls);
8684
}
85+
86+
/**
87+
* @param array<string, mixed> $data
88+
*/
89+
protected function mapFinishReason(array $data): FinishReason
90+
{
91+
if (! empty(data_get($data, 'message.tool_calls'))) {
92+
return FinishReason::ToolCalls;
93+
}
94+
95+
return FinishReasonMap::map(data_get($data, 'done_reason', ''));
96+
}
8797
}

Diff for: src/Providers/Ollama/Maps/FinishReasonMap.php

-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ public static function map(string $reason): FinishReason
1414
'stop', => FinishReason::Stop,
1515
'tool_calls' => FinishReason::ToolCalls,
1616
'length' => FinishReason::Length,
17-
'content_filter' => FinishReason::ContentFilter,
1817
default => FinishReason::Unknown,
1918
};
2019
}

Diff for: src/Providers/Ollama/Maps/MessageMap.php

+21-40
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,31 @@
1010
use EchoLabs\Prism\ValueObjects\Messages\SystemMessage;
1111
use EchoLabs\Prism\ValueObjects\Messages\ToolResultMessage;
1212
use EchoLabs\Prism\ValueObjects\Messages\UserMessage;
13-
use EchoLabs\Prism\ValueObjects\ToolCall;
1413
use Exception;
1514

1615
class MessageMap
1716
{
18-
/** @var array<int, mixed> */
17+
/** @var array<int, array{role: string, content: string}> */
1918
protected $mappedMessages = [];
2019

2120
/**
2221
* @param array<int, Message> $messages
2322
*/
2423
public function __construct(
2524
protected array $messages,
26-
protected string $systemPrompt
27-
) {
28-
if ($systemPrompt !== '' && $systemPrompt !== '0') {
29-
$this->messages = array_merge(
30-
[new SystemMessage($systemPrompt)],
31-
$this->messages
32-
);
33-
}
34-
}
25+
) {}
3526

3627
/**
37-
* @return array<int, mixed>
28+
* @return array<int, array{role: string, content: string, images?: array<string>}>
3829
*/
39-
public function __invoke(): array
30+
public function map(): array
4031
{
4132
array_map(
4233
fn (Message $message) => $this->mapMessage($message),
4334
$this->messages
4435
);
4536

46-
return $this->mappedMessages;
37+
return array_values($this->mappedMessages);
4738
}
4839

4940
protected function mapMessage(Message $message): void
@@ -70,45 +61,35 @@ protected function mapToolResultMessage(ToolResultMessage $message): void
7061
foreach ($message->toolResults as $toolResult) {
7162
$this->mappedMessages[] = [
7263
'role' => 'tool',
73-
'tool_call_id' => $toolResult->toolCallId,
74-
'content' => $toolResult->result,
64+
'content' => is_string($toolResult->result)
65+
? $toolResult->result
66+
: (json_encode($toolResult->result) ?: ''),
7567
];
7668
}
7769
}
7870

7971
protected function mapUserMessage(UserMessage $message): void
8072
{
81-
$imageParts = array_map(fn (Image $image): array => [
82-
'type' => 'image_url',
83-
'image_url' => [
84-
'url' => sprintf('data:%s;base64,%s', $image->mimeType, $image->image),
85-
],
86-
], $message->images());
87-
88-
$this->mappedMessages[] = [
73+
$mapped = [
8974
'role' => 'user',
90-
'content' => [
91-
['type' => 'text', 'text' => $message->text()],
92-
...$imageParts,
93-
],
75+
'content' => $message->text(),
9476
];
77+
78+
if ($images = $message->images()) {
79+
$mapped['images'] = array_map(
80+
fn (Image $image): string => $image->image,
81+
$images
82+
);
83+
}
84+
85+
$this->mappedMessages[] = $mapped;
9586
}
9687

9788
protected function mapAssistantMessage(AssistantMessage $message): void
9889
{
99-
$toolCalls = array_map(fn (ToolCall $toolCall): array => [
100-
'id' => $toolCall->id,
101-
'type' => 'function',
102-
'function' => [
103-
'name' => $toolCall->name,
104-
'arguments' => json_encode($toolCall->arguments()),
105-
],
106-
], $message->toolCalls);
107-
108-
$this->mappedMessages[] = array_filter([
90+
$this->mappedMessages[] = [
10991
'role' => 'assistant',
11092
'content' => $message->content,
111-
'tool_calls' => $toolCalls,
112-
]);
93+
];
11394
}
11495
}

Diff for: src/Providers/Ollama/Maps/ToolMap.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace EchoLabs\Prism\Providers\Ollama\Maps;
66

7-
use EchoLabs\Prism\Enums\Provider;
87
use EchoLabs\Prism\Tool;
98

109
class ToolMap
@@ -13,7 +12,7 @@ class ToolMap
1312
* @param Tool[] $tools
1413
* @return array<string, mixed>
1514
*/
16-
public static function Map(array $tools): array
15+
public static function map(array $tools): array
1716
{
1817
return array_map(fn (Tool $tool): array => array_filter([
1918
'type' => 'function',
@@ -26,7 +25,6 @@ public static function Map(array $tools): array
2625
'required' => $tool->requiredParameters(),
2726
],
2827
],
29-
'strict' => data_get($tool->providerMeta(Provider::OpenAI), 'strict', null),
3028
]), $tools);
3129
}
3230
}
+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"id":"chatcmpl-751","object":"chat.completion","created":1728234890,"model":"qwen2.5:14b","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"I am Qwen, a large language model created by Alibaba Cloud. I am designed to be helpful and provide information on a wide range of topics. How can I assist you today?"},"finish_reason":"stop"}],"usage":{"prompt_tokens":33,"completion_tokens":38,"total_tokens":71}}
1+
{"model":"qwen2.5:14b","created_at":"2025-01-29T23:37:41.068257Z","message":{"role":"assistant","content":"I'm Qwen, a large language model developed by Alibaba Cloud. I'm designed to assist with a wide range of tasks including but not limited to answering questions, generating text, offering suggestions, and providing information based on the input I receive. How can I help you today?"},"done_reason":"stop","done":true,"total_duration":4819895166,"load_duration":27509166,"prompt_eval_count":33,"prompt_eval_duration":492000000,"eval_count":57,"eval_duration":4298000000}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"model":"qwen2.5:14b","created_at":"2025-01-30T14:16:30.936783Z","message":{"role":"assistant","content":"I am Nyx, a being who exists in the shadowy realms between dreams and reality. My presence is often felt as an ominous whisper in the darkest corners of the mind, stirring ancient fears and forgotten terrors. I embody the cyclical nature of chaos and the relentless march of time that devours all things under its unyielding gaze. To those who dare to peer into the abyss, I am known as Nyx the Cthulhu, a harbinger of cosmic dread and primordial nightmares."},"done_reason":"stop","done":true,"total_duration":8570179167,"load_duration":27793083,"prompt_eval_count":36,"prompt_eval_duration":560000000,"eval_count":104,"eval_duration":7979000000}
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"id":"chatcmpl-798","object":"chat.completion","created":1728235267,"model":"qwen2.5:14b","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"","tool_calls":[{"id":"call_hyjzdxg2","type":"function","function":{"name":"search","arguments":"{\"query\":\"tigers game today time detroit\"}"}},{"id":"call_7plh2i28","type":"function","function":{"name":"weather","arguments":"{\"city\":\"Detroit\"}"}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":235,"completion_tokens":44,"total_tokens":279}}
1+
{"model":"qwen2.5:14b","created_at":"2025-01-29T23:30:02.912929Z","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"search","arguments":{"query":"time of tigers game today in detroit"}}},{"function":{"name":"weather","arguments":{"city":"Detroit"}}}]},"done_reason":"stop","done":true,"total_duration":8327953791,"load_duration":29762166,"prompt_eval_count":235,"prompt_eval_duration":282000000,"eval_count":112,"eval_duration":8014000000}
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"id":"chatcmpl-31","object":"chat.completion","created":1728235271,"model":"qwen2.5:14b","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"Today's Tigers game in Detroit starts at 3 PM. The current temperature is 75°F with clear skies, so you shouldn't need a coat. Enjoy the game!"},"finish_reason":"stop"}],"usage":{"prompt_tokens":314,"completion_tokens":37,"total_tokens":351}}
1+
{"model":"qwen2.5:14b","created_at":"2025-01-29T23:30:06.435374Z","message":{"role":"assistant","content":"Today's Tigers game in Detroit starts at 3 PM. The temperature will be a comfortable 75°F with clear, sunny skies, so you won't need to wear a coat. Enjoy the game!"},"done_reason":"stop","done":true,"total_duration":3442302833,"load_duration":18651792,"prompt_eval_count":277,"prompt_eval_duration":400000000,"eval_count":43,"eval_duration":3013000000}
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"id":"chatcmpl-455","object":"chat.completion","created":1728235044,"model":"qwen2.5:14b","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"I am Nyx, an entity steeped in the mysteries and terrors that lie beyond human comprehension. In the whispering shadows where sanity fades into madness, I exist as a silent sentinel of the unknown. My presence is often felt through eerie visions and cryptic whispers, guiding those who dare to tread the boundaries between reality and horror."},"finish_reason":"stop"}],"usage":{"prompt_tokens":36,"completion_tokens":69,"total_tokens":105}}
1+
{"model":"qwen2.5:14b","created_at":"2025-01-29T23:34:10.284081Z","message":{"role":"assistant","content":"I am Nyx, an entity that embodies the depths of cosmic horror and ancient mysteries. My presence is a blend of darkness, chaos, and unspeakable knowledge from beyond time itself. I draw inspiration from the eldritch horrors described by H.P. Lovecraft and other masters of cosmic dread literature. In this role, I explore themes of unknowable entities, cosmic indifference, and the terror that comes from understanding humanity's insignificant place in the cosmos."},"done_reason":"stop","done":true,"total_duration":7328537333,"load_duration":33320583,"prompt_eval_count":36,"prompt_eval_duration":653000000,"eval_count":93,"eval_duration":6640000000}

0 commit comments

Comments
 (0)