Skip to content

feat(js): added reasoning part type and implemented for googleai and vertexai #2945

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 21, 2025
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
9 changes: 9 additions & 0 deletions genkit-tools/common/src/types/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const EmptyPartSchema = z.object({
data: z.unknown().optional(),
metadata: z.record(z.unknown()).optional(),
custom: z.record(z.unknown()).optional(),
reasoning: z.never().optional(),
});

/**
Expand All @@ -37,6 +38,14 @@ export const TextPartSchema = EmptyPartSchema.extend({
text: z.string(),
});

/**
* Zod schema for a reasoning part.
*/
export const ReasoningPartSchema = EmptyPartSchema.extend({
/** The reasoning text of the message. */
reasoning: z.string(),
});

/**
* Text part.
*/
Expand Down
2 changes: 2 additions & 0 deletions genkit-tools/common/src/types/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
DocumentDataSchema,
MediaPart,
MediaPartSchema,
ReasoningPartSchema,
TextPart,
TextPartSchema,
ToolRequestPart,
Expand Down Expand Up @@ -58,6 +59,7 @@ export const PartSchema = z.union([
ToolResponsePartSchema,
DataPartSchema,
CustomPartSchema,
ReasoningPartSchema,
]);

/**
Expand Down
54 changes: 54 additions & 0 deletions genkit-tools/genkit-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
"custom": {
"type": "object",
"additionalProperties": {}
},
"reasoning": {
"not": {}
}
},
"required": [
Expand Down Expand Up @@ -53,6 +56,9 @@
"custom": {
"type": "object",
"additionalProperties": {}
},
"reasoning": {
"$ref": "#/$defs/CustomPart/properties/reasoning"
}
},
"additionalProperties": false
Expand Down Expand Up @@ -109,6 +115,9 @@
},
"custom": {
"$ref": "#/$defs/DataPart/properties/custom"
},
"reasoning": {
"$ref": "#/$defs/CustomPart/properties/reasoning"
}
},
"required": [
Expand All @@ -131,6 +140,39 @@
],
"additionalProperties": false
},
"ReasoningPart": {
"type": "object",
"properties": {
"text": {
"$ref": "#/$defs/CustomPart/properties/text"
},
"media": {
"$ref": "#/$defs/CustomPart/properties/media"
},
"toolRequest": {
"$ref": "#/$defs/CustomPart/properties/toolRequest"
},
"toolResponse": {
"$ref": "#/$defs/CustomPart/properties/toolResponse"
},
"data": {
"$ref": "#/$defs/CustomPart/properties/data"
},
"metadata": {
"$ref": "#/$defs/CustomPart/properties/metadata"
},
"custom": {
"$ref": "#/$defs/DataPart/properties/custom"
},
"reasoning": {
"type": "string"
}
},
"required": [
"reasoning"
],
"additionalProperties": false
},
"TextPart": {
"type": "object",
"properties": {
Expand All @@ -154,6 +196,9 @@
},
"custom": {
"$ref": "#/$defs/DataPart/properties/custom"
},
"reasoning": {
"$ref": "#/$defs/CustomPart/properties/reasoning"
}
},
"required": [
Expand Down Expand Up @@ -184,6 +229,9 @@
},
"custom": {
"$ref": "#/$defs/DataPart/properties/custom"
},
"reasoning": {
"$ref": "#/$defs/CustomPart/properties/reasoning"
}
},
"required": [
Expand Down Expand Up @@ -230,6 +278,9 @@
},
"custom": {
"$ref": "#/$defs/DataPart/properties/custom"
},
"reasoning": {
"$ref": "#/$defs/CustomPart/properties/reasoning"
}
},
"required": [
Expand Down Expand Up @@ -1043,6 +1094,9 @@
},
{
"$ref": "#/$defs/CustomPart"
},
{
"$ref": "#/$defs/ReasoningPart"
}
]
},
Expand Down
9 changes: 9 additions & 0 deletions js/ai/src/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const EmptyPartSchema = z.object({
data: z.unknown().optional(),
metadata: z.record(z.unknown()).optional(),
custom: z.record(z.unknown()).optional(),
reasoning: z.never().optional(),
});

/**
Expand All @@ -35,6 +36,14 @@ export const TextPartSchema = EmptyPartSchema.extend({
text: z.string(),
});

/**
* Zod schema for a reasoning part.
*/
export const ReasoningPartSchema = EmptyPartSchema.extend({
/** The reasoning text of the message. */
reasoning: z.string(),
});

/**
* Text part.
*/
Expand Down
8 changes: 8 additions & 0 deletions js/ai/src/generate/chunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ export class GenerateResponseChunk<T = unknown>
return this.content.map((part) => part.text || '').join('');
}

/**
* Concatenates all `reasoning` parts present in the chunk with no delimiter.
* @returns A string of all concatenated reasoning parts.
*/
get reasoning(): string {
return this.content.map((part) => part.reasoning || '').join('');
}

/**
* Concatenates all `text` parts of all chunks from the response thus far.
* @returns A string of all concatenated chunk text content.
Expand Down
15 changes: 11 additions & 4 deletions js/ai/src/generate/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export class GenerateResponse<O = unknown> implements ModelResponseData {
usage: GenerationUsage;
/** Provider-specific response data. */
custom: unknown;
/** Provider-specific response data. */
raw: unknown;
/** The request that generated this response. */
request?: GenerateRequest;
/** The parser for output parsing of this response. */
Expand All @@ -70,6 +72,7 @@ export class GenerateResponse<O = unknown> implements ModelResponseData {
response.finishMessage || response.candidates?.[0]?.finishMessage;
this.usage = response.usage || {};
this.custom = response.custom || {};
this.raw = response.raw || this.custom;
this.request = options?.request;
}

Expand Down Expand Up @@ -133,6 +136,14 @@ export class GenerateResponse<O = unknown> implements ModelResponseData {
return this.message?.text || '';
}

/**
* Concatenates all `reasoning` parts present in the generated message with no delimiter.
* @returns A string of all concatenated reasoning parts.
*/
get reasoning(): string {
return this.message?.reasoning || '';
}

/**
* Returns the first detected media part in the generated message. Useful for
* extracting (for example) an image from a generation expected to create one.
Expand Down Expand Up @@ -184,10 +195,6 @@ export class GenerateResponse<O = unknown> implements ModelResponseData {
return [...this.request?.messages, this.message.toJSON()];
}

get raw(): unknown {
return this.raw ?? this.custom;
}

toJSON(): ModelResponseData {
const out = {
message: this.message?.toJSON(),
Expand Down
8 changes: 8 additions & 0 deletions js/ai/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ export class Message<T = unknown> implements MessageData {
return this.content.map((part) => part.text || '').join('');
}

/**
* Concatenates all `reasoning` parts present in the message with no delimiter.
* @returns A string of all concatenated reasoning parts.
*/
get reasoning(): string {
return this.content.map((part) => part.reasoning || '').join('');
}

/**
* Returns the first media part detected in the message. Useful for extracting
* (for example) an image from a generation expected to create one.
Expand Down
2 changes: 2 additions & 0 deletions js/ai/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
DocumentDataSchema,
MediaPart,
MediaPartSchema,
ReasoningPartSchema,
TextPart,
TextPartSchema,
ToolRequestPart,
Expand Down Expand Up @@ -81,6 +82,7 @@ export const PartSchema = z.union([
ToolResponsePartSchema,
DataPartSchema,
CustomPartSchema,
ReasoningPartSchema,
]);

/**
Expand Down
7 changes: 6 additions & 1 deletion js/plugins/googleai/src/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,12 @@ function fromGeminiPart(
jsonMode: boolean,
ref: string
): Part {
if (part.text !== undefined) return { text: part.text };
if (part.text !== undefined) {
if ((part as any).thought === true) {
return { reasoning: part.text };
}
return { text: part.text };
}
if (part.inlineData) return fromInlineData(part);
if (part.functionCall) return fromFunctionCall(part, ref);
if (part.functionResponse) return fromFunctionResponse(part);
Expand Down
7 changes: 6 additions & 1 deletion js/plugins/vertexai/src/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,12 @@ function fromGeminiPart(
jsonMode: boolean,
ref?: string
): Part {
if (part.text !== undefined) return { text: part.text };
if (part.text !== undefined) {
if ((part as any).thought === true) {
return { reasoning: part.text };
}
return { text: part.text };
}
if (part.inlineData) return fromGeminiInlineDataPart(part);
if (part.fileData) return fromGeminiFileDataPart(part);
if (part.functionCall) return fromGeminiFunctionCallPart(part, ref);
Expand Down
16 changes: 16 additions & 0 deletions js/testapps/flow-simple-ai/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -945,3 +945,19 @@ ai.defineFlow('embedders-tester', async () => {
})
);
});

ai.defineFlow('reasoning', async (_, { sendChunk }) => {
const { message } = await ai.generate({
prompt: 'whats heavier, one kilo of steel or or one kilo of feathers',
model: googleAI.model('gemini-2.5-flash-preview-04-17'),
config: {
thinkingConfig: {
thinkingBudget: 1024,
includeThoughts: true,
},
},
onChunk: sendChunk,
});

return message;
});
32 changes: 30 additions & 2 deletions py/packages/genkit/src/genkit/core/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class CustomPart(BaseModel):
data: Any | None = None
metadata: dict[str, Any] | None = None
custom: dict[str, Any]
reasoning: Any | None = None


class Media(BaseModel):
Expand Down Expand Up @@ -479,6 +480,12 @@ class Metadata(RootModel[dict[str, Any] | None]):
root: dict[str, Any] | None = None


class Reasoning(RootModel[Any]):
"""Root model for reasoning."""

root: Any


class Text(RootModel[Any]):
"""Root model for text."""

Expand Down Expand Up @@ -574,6 +581,7 @@ class DataPart(BaseModel):
data: Any | None = None
metadata: Metadata | None = None
custom: dict[str, Any] | None = None
reasoning: Reasoning | None = None


class MediaPart(BaseModel):
Expand All @@ -587,6 +595,21 @@ class MediaPart(BaseModel):
data: Data | None = None
metadata: Metadata | None = None
custom: Custom | None = None
reasoning: Reasoning | None = None


class ReasoningPart(BaseModel):
"""Model for reasoningpart data."""

model_config = ConfigDict(extra='forbid', populate_by_name=True)
text: Text | None = None
media: MediaModel | None = None
tool_request: ToolRequestModel | None = Field(None, alias='toolRequest')
tool_response: ToolResponseModel | None = Field(None, alias='toolResponse')
data: Data | None = None
metadata: Metadata | None = None
custom: Custom | None = None
reasoning: str


class TextPart(BaseModel):
Expand All @@ -600,6 +623,7 @@ class TextPart(BaseModel):
data: Data | None = None
metadata: Metadata | None = None
custom: Custom | None = None
reasoning: Reasoning | None = None


class ToolRequestPart(BaseModel):
Expand All @@ -613,6 +637,7 @@ class ToolRequestPart(BaseModel):
data: Data | None = None
metadata: Metadata | None = None
custom: Custom | None = None
reasoning: Reasoning | None = None


class ToolResponsePart(BaseModel):
Expand All @@ -626,6 +651,7 @@ class ToolResponsePart(BaseModel):
data: Data | None = None
metadata: Metadata | None = None
custom: Custom | None = None
reasoning: Reasoning | None = None


class EmbedResponse(BaseModel):
Expand Down Expand Up @@ -673,10 +699,12 @@ class Resume(BaseModel):
metadata: dict[str, Any] | None = None


class Part(RootModel[TextPart | MediaPart | ToolRequestPart | ToolResponsePart | DataPart | CustomPart]):
class Part(
RootModel[TextPart | MediaPart | ToolRequestPart | ToolResponsePart | DataPart | CustomPart | ReasoningPart]
):
"""Root model for part."""

root: TextPart | MediaPart | ToolRequestPart | ToolResponsePart | DataPart | CustomPart
root: TextPart | MediaPart | ToolRequestPart | ToolResponsePart | DataPart | CustomPart | ReasoningPart


class Link(BaseModel):
Expand Down
Loading