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
50 changes: 45 additions & 5 deletions src/Messages/DTO/MessagePart.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* @phpstan-type MessagePartArrayShape array{
* channel: string,
* type: string,
* thoughtSignature?: string,
* text?: string,
* file?: FileArrayShape,
* functionCall?: FunctionCallArrayShape,
Expand All @@ -40,6 +41,7 @@ class MessagePart extends AbstractDataTransferObject
{
public const KEY_CHANNEL = 'channel';
public const KEY_TYPE = 'type';
public const KEY_THOUGHT_SIGNATURE = 'thoughtSignature';
public const KEY_TEXT = 'text';
public const KEY_FILE = 'file';
public const KEY_FUNCTION_CALL = 'functionCall';
Expand All @@ -55,6 +57,11 @@ class MessagePart extends AbstractDataTransferObject
*/
private MessagePartTypeEnum $type;

/**
* @var string|null Thought signature for extended thinking.
*/
private ?string $thoughtSignature = null;

/**
* @var string|null Text content (when type is TEXT).
*/
Expand Down Expand Up @@ -82,11 +89,13 @@ class MessagePart extends AbstractDataTransferObject
*
* @param mixed $content The content of this message part.
* @param MessagePartChannelEnum|null $channel The channel this part belongs to. Defaults to CONTENT.
* @param string|null $thoughtSignature Optional thought signature for extended thinking.
* @throws InvalidArgumentException If an unsupported content type is provided.
*/
public function __construct($content, ?MessagePartChannelEnum $channel = null)
public function __construct($content, ?MessagePartChannelEnum $channel = null, ?string $thoughtSignature = null)
{
$this->channel = $channel ?? MessagePartChannelEnum::content();
$this->thoughtSignature = $thoughtSignature;

if (is_string($content)) {
$this->type = MessagePartTypeEnum::text();
Expand Down Expand Up @@ -136,6 +145,18 @@ public function getType(): MessagePartTypeEnum
return $this->type;
}

/**
* Gets the thought signature.
*
* @since n.e.x.t
*
* @return string|null The thought signature or null if not set.
*/
public function getThoughtSignature(): ?string
{
return $this->thoughtSignature;
}

/**
* Gets the text content.
*
Expand Down Expand Up @@ -197,6 +218,11 @@ public static function getJsonSchema(): array
'description' => 'The channel this message part belongs to.',
];

$thoughtSignatureSchema = [
'type' => 'string',
'description' => 'Thought signature for extended thinking.',
];

return [
'oneOf' => [
[
Expand All @@ -211,6 +237,7 @@ public static function getJsonSchema(): array
'type' => 'string',
'description' => 'Text content.',
],
self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema,
],
'required' => [self::KEY_TYPE, self::KEY_TEXT],
'additionalProperties' => false,
Expand All @@ -224,6 +251,7 @@ public static function getJsonSchema(): array
'const' => MessagePartTypeEnum::file()->value,
],
self::KEY_FILE => File::getJsonSchema(),
self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema,
],
'required' => [self::KEY_TYPE, self::KEY_FILE],
'additionalProperties' => false,
Expand All @@ -237,6 +265,7 @@ public static function getJsonSchema(): array
'const' => MessagePartTypeEnum::functionCall()->value,
],
self::KEY_FUNCTION_CALL => FunctionCall::getJsonSchema(),
self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema,
],
'required' => [self::KEY_TYPE, self::KEY_FUNCTION_CALL],
'additionalProperties' => false,
Expand All @@ -250,6 +279,7 @@ public static function getJsonSchema(): array
'const' => MessagePartTypeEnum::functionResponse()->value,
],
self::KEY_FUNCTION_RESPONSE => FunctionResponse::getJsonSchema(),
self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema,
],
'required' => [self::KEY_TYPE, self::KEY_FUNCTION_RESPONSE],
'additionalProperties' => false,
Expand Down Expand Up @@ -287,6 +317,10 @@ public function toArray(): array
);
}

if ($this->thoughtSignature !== null) {
$data[self::KEY_THOUGHT_SIGNATURE] = $this->thoughtSignature;
}

return $data;
}

Expand All @@ -303,15 +337,21 @@ public static function fromArray(array $array): self
$channel = null;
}

$thoughtSignature = $array[self::KEY_THOUGHT_SIGNATURE] ?? null;

// Check which properties are set to determine how to construct the MessagePart
if (isset($array[self::KEY_TEXT])) {
return new self($array[self::KEY_TEXT], $channel);
return new self($array[self::KEY_TEXT], $channel, $thoughtSignature);
} elseif (isset($array[self::KEY_FILE])) {
return new self(File::fromArray($array[self::KEY_FILE]), $channel);
return new self(File::fromArray($array[self::KEY_FILE]), $channel, $thoughtSignature);
} elseif (isset($array[self::KEY_FUNCTION_CALL])) {
return new self(FunctionCall::fromArray($array[self::KEY_FUNCTION_CALL]), $channel);
return new self(FunctionCall::fromArray($array[self::KEY_FUNCTION_CALL]), $channel, $thoughtSignature);
} elseif (isset($array[self::KEY_FUNCTION_RESPONSE])) {
return new self(FunctionResponse::fromArray($array[self::KEY_FUNCTION_RESPONSE]), $channel);
return new self(
FunctionResponse::fromArray($array[self::KEY_FUNCTION_RESPONSE]),
$channel,
$thoughtSignature
);
} else {
throw new InvalidArgumentException(
'MessagePart requires one of: text, file, functionCall, or functionResponse.'
Expand Down
51 changes: 43 additions & 8 deletions src/Results/DTO/TokenUsage.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@
* This DTO tracks the number of tokens used in prompts and completions,
* which is important for monitoring usage and costs.
*
* Note that thought tokens are a subset of completion tokens, not additive.
* In other words: completionTokens - thoughtTokens = tokens of actual output content.
*
* @since 0.1.0
*
* @phpstan-type TokenUsageArrayShape array{
* promptTokens: int,
* completionTokens: int,
* totalTokens: int
* totalTokens: int,
* thoughtTokens?: int
* }
*
* @extends AbstractDataTransferObject<TokenUsageArrayShape>
Expand All @@ -27,13 +31,14 @@ class TokenUsage extends AbstractDataTransferObject
public const KEY_PROMPT_TOKENS = 'promptTokens';
public const KEY_COMPLETION_TOKENS = 'completionTokens';
public const KEY_TOTAL_TOKENS = 'totalTokens';
public const KEY_THOUGHT_TOKENS = 'thoughtTokens';
/**
* @var int Number of tokens in the prompt.
*/
private int $promptTokens;

/**
* @var int Number of tokens in the completion.
* @var int Number of tokens in the completion, including any thought tokens.
*/
private int $completionTokens;

Expand All @@ -42,20 +47,27 @@ class TokenUsage extends AbstractDataTransferObject
*/
private int $totalTokens;

/**
* @var int|null Number of tokens used for thinking, as a subset of completion tokens.
*/
private ?int $thoughtTokens;

/**
* Constructor.
*
* @since 0.1.0
*
* @param int $promptTokens Number of tokens in the prompt.
* @param int $completionTokens Number of tokens in the completion.
* @param int $completionTokens Number of tokens in the completion, including any thought tokens.
* @param int $totalTokens Total number of tokens used.
* @param int|null $thoughtTokens Number of tokens used for thinking, as a subset of completion tokens.
*/
public function __construct(int $promptTokens, int $completionTokens, int $totalTokens)
public function __construct(int $promptTokens, int $completionTokens, int $totalTokens, ?int $thoughtTokens = null)
{
$this->promptTokens = $promptTokens;
$this->completionTokens = $completionTokens;
$this->totalTokens = $totalTokens;
$this->thoughtTokens = $thoughtTokens;
}

/**
Expand All @@ -71,7 +83,7 @@ public function getPromptTokens(): int
}

/**
* Gets the number of completion tokens.
* Gets the number of completion tokens, including any thought tokens.
*
* @since 0.1.0
*
Expand All @@ -94,6 +106,18 @@ public function getTotalTokens(): int
return $this->totalTokens;
}

/**
* Gets the number of thought tokens, which is a subset of the completion token count.
*
* @since n.e.x.t
*
* @return int|null The thought token count or null if not available.
*/
public function getThoughtTokens(): ?int
{
return $this->thoughtTokens;
}

/**
* {@inheritDoc}
*
Expand All @@ -110,12 +134,16 @@ public static function getJsonSchema(): array
],
self::KEY_COMPLETION_TOKENS => [
'type' => 'integer',
'description' => 'Number of tokens in the completion.',
'description' => 'Number of tokens in the completion, including any thought tokens.',
],
self::KEY_TOTAL_TOKENS => [
'type' => 'integer',
'description' => 'Total number of tokens used.',
],
self::KEY_THOUGHT_TOKENS => [
'type' => 'integer',
'description' => 'Number of tokens used for thinking, as a subset of completion tokens.',
],
],
'required' => [self::KEY_PROMPT_TOKENS, self::KEY_COMPLETION_TOKENS, self::KEY_TOTAL_TOKENS],
];
Expand All @@ -130,11 +158,17 @@ public static function getJsonSchema(): array
*/
public function toArray(): array
{
return [
$data = [
self::KEY_PROMPT_TOKENS => $this->promptTokens,
self::KEY_COMPLETION_TOKENS => $this->completionTokens,
self::KEY_TOTAL_TOKENS => $this->totalTokens,
];

if ($this->thoughtTokens !== null) {
$data[self::KEY_THOUGHT_TOKENS] = $this->thoughtTokens;
}

return $data;
}

/**
Expand All @@ -153,7 +187,8 @@ public static function fromArray(array $array): self
return new self(
$array[self::KEY_PROMPT_TOKENS],
$array[self::KEY_COMPLETION_TOKENS],
$array[self::KEY_TOTAL_TOKENS]
$array[self::KEY_TOTAL_TOKENS],
$array[self::KEY_THOUGHT_TOKENS] ?? null
);
}
}
Loading