diff --git a/copilot-sdk b/copilot-sdk index ccb7d5f..a552ae4 160000 --- a/copilot-sdk +++ b/copilot-sdk @@ -1 +1 @@ -Subproject commit ccb7d5f386ca43cc0d6857409f0b9d35129234d9 +Subproject commit a552ae497313143e2426d6ff7e99b2632ab573dd diff --git a/src/Client.php b/src/Client.php index 4ee79da..d414d3e 100644 --- a/src/Client.php +++ b/src/Client.php @@ -235,6 +235,7 @@ public function createSession(SessionConfig|array $config = []): CopilotSession $response = $this->rpcClient->request('session.create', array_filter([ 'sessionId' => $config['sessionId'] ?? null, 'model' => $config['model'] ?? null, + 'reasoningEffort' => $config['reasoningEffort'] ?? null, 'tools' => $toolsForRequest ?: null, 'systemMessage' => $config['systemMessage'] ?? null, 'availableTools' => $config['availableTools'] ?? null, @@ -309,6 +310,7 @@ public function resumeSession(string $sessionId, ResumeSessionConfig|array $conf $response = $this->rpcClient->request('session.resume', array_filter([ 'sessionId' => $sessionId, + 'reasoningEffort' => $config['reasoningEffort'] ?? null, 'tools' => $toolsForRequest ?: null, 'provider' => $config['provider'] ?? null, 'requestPermission' => isset($config['onPermissionRequest']), diff --git a/src/Enums/ReasoningEffort.php b/src/Enums/ReasoningEffort.php new file mode 100644 index 0000000..aa7f93f --- /dev/null +++ b/src/Enums/ReasoningEffort.php @@ -0,0 +1,19 @@ +supports['vision'] ?? false; } + /** + * Check if reasoning effort is supported. + */ + public function supportsReasoningEffort(): bool + { + return $this->supports['reasoningEffort'] ?? false; + } + /** * Get max context window tokens. */ diff --git a/src/Types/ModelInfo.php b/src/Types/ModelInfo.php index ad2fb80..df18ba1 100644 --- a/src/Types/ModelInfo.php +++ b/src/Types/ModelInfo.php @@ -5,6 +5,7 @@ namespace Revolution\Copilot\Types; use Illuminate\Contracts\Support\Arrayable; +use Revolution\Copilot\Enums\ReasoningEffort; /** * Information about an available model. @@ -22,21 +23,34 @@ public function __construct( public ?ModelPolicy $policy = null, /** Billing information */ public ?ModelBilling $billing = null, + /** Supported reasoning effort levels (only present if model supports reasoning effort) */ + public ?array $supportedReasoningEfforts = null, + /** Default reasoning effort level (only present if model supports reasoning effort) */ + public ReasoningEffort|string|null $defaultReasoningEffort = null, ) {} /** * Create from array. * - * @param array{id: string, name: string, capabilities: array, policy?: array, billing?: array} $data + * @param array{id: string, name: string, capabilities: array, policy?: array, billing?: array, supportedReasoningEfforts?: array, defaultReasoningEffort?: string} $data */ public static function fromArray(array $data): self { + $defaultReasoningEffort = null; + if (isset($data['defaultReasoningEffort'])) { + $defaultReasoningEffort = is_string($data['defaultReasoningEffort']) + ? $data['defaultReasoningEffort'] + : $data['defaultReasoningEffort']; + } + return new self( id: $data['id'], name: $data['name'], capabilities: ModelCapabilities::fromArray($data['capabilities']), policy: isset($data['policy']) ? ModelPolicy::fromArray($data['policy']) : null, billing: isset($data['billing']) ? ModelBilling::fromArray($data['billing']) : null, + supportedReasoningEfforts: $data['supportedReasoningEfforts'] ?? null, + defaultReasoningEffort: $defaultReasoningEffort, ); } @@ -45,12 +59,18 @@ public static function fromArray(array $data): self */ public function toArray(): array { + $defaultReasoningEffort = $this->defaultReasoningEffort instanceof ReasoningEffort + ? $this->defaultReasoningEffort->value + : $this->defaultReasoningEffort; + return array_filter([ 'id' => $this->id, 'name' => $this->name, 'capabilities' => $this->capabilities->toArray(), 'policy' => $this->policy?->toArray(), 'billing' => $this->billing?->toArray(), + 'supportedReasoningEfforts' => $this->supportedReasoningEfforts, + 'defaultReasoningEffort' => $defaultReasoningEffort, ], fn ($v) => $v !== null); } } diff --git a/src/Types/ResumeSessionConfig.php b/src/Types/ResumeSessionConfig.php index aec80e2..164228e 100644 --- a/src/Types/ResumeSessionConfig.php +++ b/src/Types/ResumeSessionConfig.php @@ -6,6 +6,7 @@ use Closure; use Illuminate\Contracts\Support\Arrayable; +use Revolution\Copilot\Enums\ReasoningEffort; /** * Configuration for resuming a session. @@ -13,6 +14,12 @@ readonly class ResumeSessionConfig implements Arrayable { public function __construct( + /** + * Reasoning effort level for models that support it. + * Only valid for models where capabilities.supports.reasoningEffort is true. + * Accepts either ReasoningEffort enum or string value. + */ + public ReasoningEffort|string|null $reasoningEffort = null, /** * Tools exposed to the CLI server. */ @@ -79,6 +86,13 @@ public function __construct( */ public static function fromArray(array $data): self { + $reasoningEffort = null; + if (isset($data['reasoningEffort'])) { + $reasoningEffort = $data['reasoningEffort'] instanceof ReasoningEffort + ? $data['reasoningEffort'] + : $data['reasoningEffort']; + } + $provider = null; if (isset($data['provider'])) { $provider = $data['provider'] instanceof ProviderConfig @@ -94,6 +108,7 @@ public static function fromArray(array $data): self } return new self( + reasoningEffort: $reasoningEffort, tools: $data['tools'] ?? null, provider: $provider, onPermissionRequest: $data['onPermissionRequest'] ?? null, @@ -114,6 +129,10 @@ public static function fromArray(array $data): self */ public function toArray(): array { + $reasoningEffort = $this->reasoningEffort instanceof ReasoningEffort + ? $this->reasoningEffort->value + : $this->reasoningEffort; + $provider = $this->provider instanceof ProviderConfig ? $this->provider->toArray() : $this->provider; @@ -123,6 +142,7 @@ public function toArray(): array : $this->hooks; return array_filter([ + 'reasoningEffort' => $reasoningEffort, 'tools' => $this->tools, 'provider' => $provider, 'onPermissionRequest' => $this->onPermissionRequest, diff --git a/src/Types/SessionConfig.php b/src/Types/SessionConfig.php index 614177b..9626a54 100644 --- a/src/Types/SessionConfig.php +++ b/src/Types/SessionConfig.php @@ -6,6 +6,7 @@ use Closure; use Illuminate\Contracts\Support\Arrayable; +use Revolution\Copilot\Enums\ReasoningEffort; /** * Configuration for creating a session. @@ -22,6 +23,13 @@ public function __construct( * Model to use for this session. */ public ?string $model = null, + /** + * Reasoning effort level for models that support it. + * Only valid for models where capabilities.supports.reasoningEffort is true. + * Use client.listModels() to check supported values for each model. + * Accepts either ReasoningEffort enum or string value. + */ + public ReasoningEffort|string|null $reasoningEffort = null, /** * Override the default configuration directory location. * When specified, the session will use this directory for storing config and state. @@ -135,9 +143,17 @@ public static function fromArray(array $data): self : SessionHooks::fromArray($data['hooks']); } + $reasoningEffort = null; + if (isset($data['reasoningEffort'])) { + $reasoningEffort = $data['reasoningEffort'] instanceof ReasoningEffort + ? $data['reasoningEffort'] + : $data['reasoningEffort']; + } + return new self( sessionId: $data['sessionId'] ?? null, model: $data['model'] ?? null, + reasoningEffort: $reasoningEffort, configDir: $data['configDir'] ?? null, tools: $data['tools'] ?? null, systemMessage: $systemMessage, @@ -162,6 +178,10 @@ public static function fromArray(array $data): self */ public function toArray(): array { + $reasoningEffort = $this->reasoningEffort instanceof ReasoningEffort + ? $this->reasoningEffort->value + : $this->reasoningEffort; + $systemMessage = $this->systemMessage instanceof SystemMessageConfig ? $this->systemMessage->toArray() : $this->systemMessage; @@ -181,6 +201,7 @@ public function toArray(): array return array_filter([ 'sessionId' => $this->sessionId, 'model' => $this->model, + 'reasoningEffort' => $reasoningEffort, 'configDir' => $this->configDir, 'tools' => $this->tools, 'systemMessage' => $systemMessage, diff --git a/tests/Unit/Enums/ReasoningEffortTest.php b/tests/Unit/Enums/ReasoningEffortTest.php new file mode 100644 index 0000000..a270bbf --- /dev/null +++ b/tests/Unit/Enums/ReasoningEffortTest.php @@ -0,0 +1,31 @@ +value)->toBe('low') + ->and(ReasoningEffort::MEDIUM->value)->toBe('medium') + ->and(ReasoningEffort::HIGH->value)->toBe('high') + ->and(ReasoningEffort::XHIGH->value)->toBe('xhigh'); + }); + + it('can be created from string', function () { + expect(ReasoningEffort::from('low'))->toBe(ReasoningEffort::LOW) + ->and(ReasoningEffort::from('medium'))->toBe(ReasoningEffort::MEDIUM) + ->and(ReasoningEffort::from('high'))->toBe(ReasoningEffort::HIGH) + ->and(ReasoningEffort::from('xhigh'))->toBe(ReasoningEffort::XHIGH); + }); + + it('has all expected cases', function () { + $cases = ReasoningEffort::cases(); + + expect($cases)->toHaveCount(4) + ->and($cases)->toContain(ReasoningEffort::LOW) + ->and($cases)->toContain(ReasoningEffort::MEDIUM) + ->and($cases)->toContain(ReasoningEffort::HIGH) + ->and($cases)->toContain(ReasoningEffort::XHIGH); + }); +}); diff --git a/tests/Unit/Types/ModelCapabilitiesTest.php b/tests/Unit/Types/ModelCapabilitiesTest.php index b94b9ff..5001e42 100644 --- a/tests/Unit/Types/ModelCapabilitiesTest.php +++ b/tests/Unit/Types/ModelCapabilitiesTest.php @@ -68,4 +68,31 @@ expect($capabilities)->toBeInstanceOf(\Illuminate\Contracts\Support\Arrayable::class); }); + + it('can check if reasoning effort is supported', function () { + $capabilities = ModelCapabilities::fromArray([ + 'supports' => ['vision' => false, 'reasoningEffort' => true], + 'limits' => ['max_context_window_tokens' => 100000], + ]); + + expect($capabilities->supportsReasoningEffort())->toBeTrue(); + }); + + it('returns false when reasoning effort is not supported', function () { + $capabilities = ModelCapabilities::fromArray([ + 'supports' => ['vision' => false, 'reasoningEffort' => false], + 'limits' => ['max_context_window_tokens' => 100000], + ]); + + expect($capabilities->supportsReasoningEffort())->toBeFalse(); + }); + + it('returns false when reasoningEffort key is missing', function () { + $capabilities = ModelCapabilities::fromArray([ + 'supports' => ['vision' => true], + 'limits' => ['max_context_window_tokens' => 100000], + ]); + + expect($capabilities->supportsReasoningEffort())->toBeFalse(); + }); }); diff --git a/tests/Unit/Types/ModelInfoTest.php b/tests/Unit/Types/ModelInfoTest.php index 9452e42..b95611e 100644 --- a/tests/Unit/Types/ModelInfoTest.php +++ b/tests/Unit/Types/ModelInfoTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Revolution\Copilot\Enums\ReasoningEffort; use Revolution\Copilot\Types\ModelBilling; use Revolution\Copilot\Types\ModelCapabilities; use Revolution\Copilot\Types\ModelInfo; @@ -103,4 +104,69 @@ expect($modelInfo)->toBeInstanceOf(\Illuminate\Contracts\Support\Arrayable::class); }); + + it('can handle supportedReasoningEfforts array', function () { + $modelInfo = ModelInfo::fromArray([ + 'id' => 'reasoning-model', + 'name' => 'Reasoning Model', + 'capabilities' => [ + 'supports' => ['vision' => false, 'reasoningEffort' => true], + 'limits' => ['max_context_window_tokens' => 128000], + ], + 'supportedReasoningEfforts' => ['low', 'medium', 'high'], + 'defaultReasoningEffort' => 'medium', + ]); + + expect($modelInfo->supportedReasoningEfforts)->toBe(['low', 'medium', 'high']) + ->and($modelInfo->defaultReasoningEffort)->toBe('medium'); + }); + + it('can handle defaultReasoningEffort as enum', function () { + $modelInfo = new ModelInfo( + id: 'reasoning-model', + name: 'Reasoning Model', + capabilities: new ModelCapabilities( + supports: ['reasoningEffort' => true], + limits: ['max_context_window_tokens' => 128000], + ), + supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh'], + defaultReasoningEffort: ReasoningEffort::HIGH, + ); + + expect($modelInfo->defaultReasoningEffort)->toBe(ReasoningEffort::HIGH); + }); + + it('converts defaultReasoningEffort enum to string in toArray', function () { + $modelInfo = new ModelInfo( + id: 'reasoning-model', + name: 'Reasoning Model', + capabilities: new ModelCapabilities( + supports: ['reasoningEffort' => true], + limits: ['max_context_window_tokens' => 128000], + ), + supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh'], + defaultReasoningEffort: ReasoningEffort::XHIGH, + ); + + $array = $modelInfo->toArray(); + + expect($array['defaultReasoningEffort'])->toBe('xhigh') + ->and($array['supportedReasoningEfforts'])->toBe(['low', 'medium', 'high', 'xhigh']); + }); + + it('excludes reasoning effort fields when null', function () { + $modelInfo = new ModelInfo( + id: 'basic-model', + name: 'Basic Model', + capabilities: new ModelCapabilities( + supports: ['vision' => false], + limits: ['max_context_window_tokens' => 100000], + ), + ); + + $array = $modelInfo->toArray(); + + expect($array)->not->toHaveKey('supportedReasoningEfforts') + ->and($array)->not->toHaveKey('defaultReasoningEffort'); + }); }); diff --git a/tests/Unit/Types/ResumeSessionConfigTest.php b/tests/Unit/Types/ResumeSessionConfigTest.php index 67d5048..84c9160 100644 --- a/tests/Unit/Types/ResumeSessionConfigTest.php +++ b/tests/Unit/Types/ResumeSessionConfigTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Revolution\Copilot\Enums\ReasoningEffort; use Revolution\Copilot\Types\ProviderConfig; use Revolution\Copilot\Types\ResumeSessionConfig; use Revolution\Copilot\Types\SessionHooks; @@ -135,4 +136,56 @@ expect($config)->toBeInstanceOf(\Illuminate\Contracts\Support\Arrayable::class); }); + + it('accepts reasoningEffort as enum', function () { + $config = new ResumeSessionConfig( + reasoningEffort: ReasoningEffort::HIGH, + ); + + expect($config->reasoningEffort)->toBe(ReasoningEffort::HIGH); + }); + + it('accepts reasoningEffort as string', function () { + $config = new ResumeSessionConfig( + reasoningEffort: 'medium', + ); + + expect($config->reasoningEffort)->toBe('medium'); + }); + + it('converts reasoningEffort enum to string in toArray', function () { + $config = new ResumeSessionConfig( + reasoningEffort: ReasoningEffort::XHIGH, + ); + + $array = $config->toArray(); + + expect($array['reasoningEffort'])->toBe('xhigh'); + }); + + it('preserves reasoningEffort string in toArray', function () { + $config = new ResumeSessionConfig( + reasoningEffort: 'low', + ); + + $array = $config->toArray(); + + expect($array['reasoningEffort'])->toBe('low'); + }); + + it('can be created from array with reasoningEffort as string', function () { + $config = ResumeSessionConfig::fromArray([ + 'reasoningEffort' => 'high', + ]); + + expect($config->reasoningEffort)->toBe('high'); + }); + + it('preserves reasoningEffort enum when passed to fromArray', function () { + $config = ResumeSessionConfig::fromArray([ + 'reasoningEffort' => ReasoningEffort::MEDIUM, + ]); + + expect($config->reasoningEffort)->toBe(ReasoningEffort::MEDIUM); + }); }); diff --git a/tests/Unit/Types/SessionConfigTest.php b/tests/Unit/Types/SessionConfigTest.php index 4d1e17e..bae29e3 100644 --- a/tests/Unit/Types/SessionConfigTest.php +++ b/tests/Unit/Types/SessionConfigTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Revolution\Copilot\Enums\ReasoningEffort; use Revolution\Copilot\Types\InfiniteSessionConfig; use Revolution\Copilot\Types\ProviderConfig; use Revolution\Copilot\Types\SessionConfig; @@ -200,4 +201,56 @@ expect($config)->toBeInstanceOf(\Illuminate\Contracts\Support\Arrayable::class); }); + + it('accepts reasoningEffort as enum', function () { + $config = new SessionConfig( + reasoningEffort: ReasoningEffort::HIGH, + ); + + expect($config->reasoningEffort)->toBe(ReasoningEffort::HIGH); + }); + + it('accepts reasoningEffort as string', function () { + $config = new SessionConfig( + reasoningEffort: 'medium', + ); + + expect($config->reasoningEffort)->toBe('medium'); + }); + + it('converts reasoningEffort enum to string in toArray', function () { + $config = new SessionConfig( + reasoningEffort: ReasoningEffort::XHIGH, + ); + + $array = $config->toArray(); + + expect($array['reasoningEffort'])->toBe('xhigh'); + }); + + it('preserves reasoningEffort string in toArray', function () { + $config = new SessionConfig( + reasoningEffort: 'low', + ); + + $array = $config->toArray(); + + expect($array['reasoningEffort'])->toBe('low'); + }); + + it('can be created from array with reasoningEffort as string', function () { + $config = SessionConfig::fromArray([ + 'reasoningEffort' => 'high', + ]); + + expect($config->reasoningEffort)->toBe('high'); + }); + + it('preserves reasoningEffort enum when passed to fromArray', function () { + $config = SessionConfig::fromArray([ + 'reasoningEffort' => ReasoningEffort::MEDIUM, + ]); + + expect($config->reasoningEffort)->toBe(ReasoningEffort::MEDIUM); + }); });