Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 3 additions & 27 deletions includes/Models/OllamaImageGenerationModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

namespace Fueled\AiProviderForOllama\Models;

use Fueled\AiProviderForOllama\Models\Traits\OllamaRequestOptionsTrait;
use Fueled\AiProviderForOllama\Provider\OllamaProvider;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Files\DTO\File;
Expand Down Expand Up @@ -47,6 +48,7 @@
* }
*/
class OllamaImageGenerationModel extends AbstractApiBasedModel implements ImageGenerationModelInterface {
use OllamaRequestOptionsTrait;

/**
* Generates images from a prompt using the Ollama API.
Expand Down Expand Up @@ -95,33 +97,7 @@ public function generateImageResult( array $prompt ): GenerativeAiResult {
* @return \WordPress\AiClient\Providers\Http\DTO\RequestOptions Prepared request options.
*/
private function prepareRequestOptionsForImageGeneration(): RequestOptions {
$existing_options = $this->getRequestOptions();
if ( null !== $existing_options ) {
$request_options = RequestOptions::fromArray( $existing_options->toArray() );
} else {
$request_options = new RequestOptions();
}

$custom_options = $this->getConfig()->getCustomOptions();

$request_timeout = 300.0;
if ( isset( $custom_options['ollama.request_timeout'] ) && is_numeric( $custom_options['ollama.request_timeout'] ) ) {
$request_timeout = (float) $custom_options['ollama.request_timeout'];
}

$connect_timeout = 10.0;
if ( isset( $custom_options['ollama.connect_timeout'] ) && is_numeric( $custom_options['ollama.connect_timeout'] ) ) {
$connect_timeout = (float) $custom_options['ollama.connect_timeout'];
}

// Always enforce request timeout for image generation to avoid 90s defaults.
$request_options->setTimeout( $request_timeout );

if ( null === $request_options->getConnectTimeout() ) {
$request_options->setConnectTimeout( $connect_timeout );
}

return $request_options;
return $this->prepareRequestOptions( 300.0, 10.0 );
}

/**
Expand Down
54 changes: 53 additions & 1 deletion includes/Models/OllamaTextGenerationModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

namespace Fueled\AiProviderForOllama\Models;

use Fueled\AiProviderForOllama\Models\Traits\OllamaRequestOptionsTrait;
use Fueled\AiProviderForOllama\Provider\OllamaProvider;
use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Http\Enums\HttpMethodEnum;
use WordPress\AiClient\Providers\OpenAiCompatibleImplementation\AbstractOpenAiCompatibleTextGenerationModel;

Expand All @@ -17,6 +19,34 @@
* @since 1.0.0
*/
class OllamaTextGenerationModel extends AbstractOpenAiCompatibleTextGenerationModel {
use OllamaRequestOptionsTrait;

/**
* Prepares the response format parameter for Ollama's OpenAI-compatible API.
*
* Ollama's OpenAI-compatible API uses the same response_format key as OpenAI,
* but schema mode expects the schema to be nested at json_schema.schema.
*
* @since x.x.x
*
* @param array<string, mixed>|null $output_schema The output schema.
* @return array<string, mixed> The prepared response format parameter.
*/
protected function prepareResponseFormatParam( ?array $output_schema ): array {
if ( is_array( $output_schema ) ) {
return array(
'type' => 'json_schema',
'json_schema' => array(
'name' => 'response_schema',
'schema' => $output_schema,
),
);
}

return array(
'type' => 'json_object',
);
}

/**
* {@inheritDoc}
Expand All @@ -29,6 +59,13 @@ protected function createRequest(
array $headers = array(),
$data = null
): Request {
$request_options = $this->prepareRequestOptionsForTextGeneration();

// Keep transport-only timeout options out of the OpenAI-compatible payload.
if ( is_array( $data ) ) {
unset( $data['ollama.request_timeout'], $data['ollama.connect_timeout'] );
}

// Ollama supports OpenAI-compatible endpoints at /v1/.
$path = ltrim( (string) preg_replace( '#^v1/?#', '', ltrim( $path, '/' ) ), '/' );
$path = '/v1/' . $path;
Expand All @@ -38,7 +75,22 @@ protected function createRequest(
OllamaProvider::url( $path ),
$headers,
$data,
$this->getRequestOptions()
$request_options
);
}

/**
* Prepares request options for text generation with a longer default timeout.
*
* Supported custom options:
* - ollama.request_timeout (seconds)
* - ollama.connect_timeout (seconds)
*
* @since x.x.x
*
* @return \WordPress\AiClient\Providers\Http\DTO\RequestOptions Prepared request options.
*/
private function prepareRequestOptionsForTextGeneration(): RequestOptions {
return $this->prepareRequestOptions( 60.0, 10.0 );
}
}
67 changes: 67 additions & 0 deletions includes/Models/Traits/OllamaRequestOptionsTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
/**
* Shared Ollama request options preparation.
*
* @package Fueled\AiProviderForOllama\Models\Traits
* @since x.x.x
*/

declare( strict_types=1 );

namespace Fueled\AiProviderForOllama\Models\Traits;

use WordPress\AiClient\Providers\Http\DTO\RequestOptions;

/**
* Trait for preparing request options with configurable timeout defaults.
*
* @since x.x.x
*/
trait OllamaRequestOptionsTrait {

/**
* Prepares request options with timeout defaults and custom overrides.
*
* Supported custom options:
* - ollama.request_timeout (seconds)
* - ollama.connect_timeout (seconds)
*
* @since x.x.x
*
* @param float $default_request_timeout Default request timeout in seconds.
* @param float $default_connect_timeout Default connect timeout in seconds.
* @return \WordPress\AiClient\Providers\Http\DTO\RequestOptions Prepared request options.
*/
// phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
protected function prepareRequestOptions(
float $default_request_timeout,
float $default_connect_timeout
): RequestOptions {
$existing_options = $this->getRequestOptions();
if ( null !== $existing_options ) {
$request_options = RequestOptions::fromArray( $existing_options->toArray() );
} else {
$request_options = new RequestOptions();
}

$custom_options = $this->getConfig()->getCustomOptions();

$request_timeout = $default_request_timeout;
if ( isset( $custom_options['ollama.request_timeout'] ) && is_numeric( $custom_options['ollama.request_timeout'] ) ) {
$request_timeout = (float) $custom_options['ollama.request_timeout'];
}

$connect_timeout = $default_connect_timeout;
if ( isset( $custom_options['ollama.connect_timeout'] ) && is_numeric( $custom_options['ollama.connect_timeout'] ) ) {
$connect_timeout = (float) $custom_options['ollama.connect_timeout'];
}

$request_options->setTimeout( $request_timeout );

if ( null === $request_options->getConnectTimeout() ) {
$request_options->setConnectTimeout( $connect_timeout );
}

return $request_options;
}
}
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ parameters:
- includes/Metadata/OllamaModelMetadataDirectory.php
- includes/Models/OllamaImageGenerationModel.php
- includes/Models/OllamaTextGenerationModel.php
- includes/Models/Traits/OllamaRequestOptionsTrait.php
- includes/Provider/OllamaProvider.php
- includes/Settings/OllamaSettings.php
analyseAndScan:
Expand Down
10 changes: 10 additions & 0 deletions tests/Integration/Models/MockOllamaTextGenerationModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,14 @@ public function expose_create_request(
): Request {
return $this->createRequest( $method, $path, $headers, $data );
}

/**
* Publicly exposes the protected prepareResponseFormatParam() method.
*
* @param array<string, mixed>|null $output_schema The output schema.
* @return array<string, mixed> The prepared response format parameter.
*/
public function expose_prepare_response_format_param( ?array $output_schema ): array {
return $this->prepareResponseFormatParam( $output_schema );
}
}
133 changes: 128 additions & 5 deletions tests/Integration/Models/OllamaTextGenerationModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Enums\ProviderTypeEnum;
use WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Http\Enums\HttpMethodEnum;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;

/**
* Tests for OllamaTextGenerationModel path normalization in createRequest().
*
* All tests exercise the path-normalisation logic that is the only custom
* behaviour in OllamaTextGenerationModel — everything else is covered by
* the php-ai-client AbstractOpenAiCompatibleTextGenerationModel tests.
* Tests for OllamaTextGenerationModel request behavior.
*
* @covers \Fueled\AiProviderForOllama\Models\OllamaTextGenerationModel
*/
Expand Down Expand Up @@ -94,4 +92,129 @@ public function test_request_uses_provider_base_url(): void {
);
$this->assertStringStartsWith( 'http://localhost:11434', $request->getUri() );
}

/**
* Tests that JSON output without schema uses json_object response format.
*/
public function test_prepare_response_format_uses_json_object_without_schema(): void {
$response_format = $this->model->expose_prepare_response_format_param( null );

$this->assertSame(
array(
'type' => 'json_object',
),
$response_format
);
}

/**
* Tests that JSON schema output is nested at json_schema.schema.
*/
public function test_prepare_response_format_wraps_schema_for_ollama_openai_compat(): void {
$schema = array(
'type' => 'object',
'properties' => array(
'name' => array( 'type' => 'string' ),
),
'required' => array( 'name' ),
);

$response_format = $this->model->expose_prepare_response_format_param( $schema );

$this->assertSame(
array(
'type' => 'json_schema',
'json_schema' => array(
'name' => 'response_schema',
'schema' => $schema,
),
),
$response_format
);
}

/**
* Tests that text requests use longer default request/connect timeouts.
*/
public function test_default_request_timeouts_are_applied_to_text_requests(): void {
$request = $this->model->expose_create_request(
HttpMethodEnum::POST(),
'chat/completions',
array(),
array()
);

$this->assertNotNull( $request->getOptions() );
$this->assertSame( 60.0, $request->getOptions()->getTimeout() );
$this->assertSame( 10.0, $request->getOptions()->getConnectTimeout() );
}

/**
* Tests that custom timeout options are applied and removed from payload data.
*/
public function test_custom_timeouts_are_applied_and_not_sent_in_payload(): void {
$this->model->setConfig(
ModelConfig::fromArray(
array(
'customOptions' => array(
'ollama.request_timeout' => 45,
'ollama.connect_timeout' => 2,
),
)
)
);

$request = $this->model->expose_create_request(
HttpMethodEnum::POST(),
'chat/completions',
array(),
array(
'ollama.request_timeout' => 45,
'ollama.connect_timeout' => 2,
'model' => 'llama3.2',
)
);

$this->assertNotNull( $request->getOptions() );
$this->assertSame( 45.0, $request->getOptions()->getTimeout() );
$this->assertSame( 2.0, $request->getOptions()->getConnectTimeout() );
$this->assertSame(
array(
'model' => 'llama3.2',
),
$request->getData()
);
}

/**
* Tests that an existing connect timeout is preserved for text requests.
*/
public function test_existing_connect_timeout_is_preserved_for_text_requests(): void {
$request_options = new RequestOptions();
$request_options->setConnectTimeout( 6.0 );
$request_options->setTimeout( 20.0 );
$this->model->setRequestOptions( $request_options );

$this->model->setConfig(
ModelConfig::fromArray(
array(
'customOptions' => array(
'ollama.request_timeout' => 90,
'ollama.connect_timeout' => 2,
),
)
)
);

$request = $this->model->expose_create_request(
HttpMethodEnum::POST(),
'chat/completions',
array(),
array()
);

$this->assertNotNull( $request->getOptions() );
$this->assertSame( 90.0, $request->getOptions()->getTimeout() );
$this->assertSame( 6.0, $request->getOptions()->getConnectTimeout() );
}
}
Loading