19 Commits

Author SHA1 Message Date
Sean Goedecke
5022b33bc1 Merge pull request #148 from dsanders11/feat/prompt-yaml-model-parameters
feat: support modelParameters in prompt.yaml files
2025-11-24 11:27:47 +11:00
David Sanders
c9e14713bc chore: update dist 2025-11-23 16:19:48 -08:00
David Sanders
39308142df chore: apply code review comment
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-23 16:19:12 -08:00
David Sanders
48f0edec4d feat: support modelParameters in prompt.yaml files 2025-11-23 16:07:11 -08:00
Sean Goedecke
36ea1371dc Merge pull request #136 from dsanders11/fix/template-substition
fix: do template substition after parsing prompt YAML
2025-11-24 10:22:42 +11:00
Sean Goedecke
de16a30c20 Merge branch 'main' into fix/template-substition 2025-11-24 10:21:49 +11:00
Sean Goedecke
dd3dff10ba Merge pull request #147 from srt32/patch-1
Clarify PAT requirement for github-mcp-token
2025-11-24 10:18:50 +11:00
Simon Taranto
4bb01ee5ee Clarify PAT requirement for github-mcp-token
I mistakenly read the description of the mcp-token field to mean I needed a "PAT for MCP" as if there were a PAT permission for MCP. This change clarifies the language.
2025-11-21 13:36:45 -05:00
David Sanders
af1c1c29a3 fix: do template substition after parsing prompt YAML 2025-10-20 21:32:06 -07:00
Sean Goedecke
83bb5ca3e8 Merge pull request #93 from FidelusAleksander/main
docs: update documentation on mcp usage
2025-08-26 18:13:39 +10:00
Aleksander Fidelus
4d2337d006 Merge branch 'actions:main' into main 2025-08-25 11:08:41 +02:00
Yuzuki
7ba7530ad4 Merge pull request #94 from actions/dependabot/github_actions/actions/checkout-5
chore(deps): bump actions/checkout from 4 to 5
2025-08-25 14:00:39 +10:00
Yuzuki
4d7d83c494 Merge branch 'main' into dependabot/github_actions/actions/checkout-5 2025-08-25 13:55:57 +10:00
Sean Goedecke
a1c1182922 Merge pull request #97 from actions/sgoedecke/defensive-parsing
Parse inference response format defensively
2025-08-25 08:47:18 +10:00
Sean Goedecke
dfaa426c29 Parse inference response format defensively 2025-08-22 22:34:18 +00:00
FidelusAleksander
7fa0024f13 docs: run prettier 2025-08-18 14:42:29 +02:00
dependabot[bot]
fc6f9a0800 chore(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-18 03:58:02 +00:00
FidelusAleksander
a1d07305b7 docs: update github-mcp-token description 2025-08-15 08:22:55 +02:00
FidelusAleksander
6e0d8949d8 docs: update documentation on mcp usage 2025-08-15 07:52:22 +02:00
14 changed files with 156 additions and 79 deletions

View File

@@ -28,7 +28,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Node.js - name: Setup Node.js
id: setup-node id: setup-node

View File

@@ -20,7 +20,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Node.js - name: Setup Node.js
id: setup-node id: setup-node
@@ -54,7 +54,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Test Local Action - name: Test Local Action
id: test-action id: test-action
@@ -77,7 +77,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Create Prompt File - name: Create Prompt File
run: echo "hello" > prompt.txt run: echo "hello" > prompt.txt

View File

@@ -30,7 +30,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Initialize CodeQL - name: Initialize CodeQL
id: initialize id: initialize

View File

@@ -27,7 +27,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Node.js - name: Setup Node.js
id: setup-node id: setup-node

View File

@@ -21,7 +21,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0

View File

@@ -162,6 +162,9 @@ This action now supports **read-only** integration with the GitHub-hosted Model
Context Protocol (MCP) server, which provides access to GitHub tools like Context Protocol (MCP) server, which provides access to GitHub tools like
repository management, issue tracking, and pull request operations. repository management, issue tracking, and pull request operations.
> [!NOTE]
> The GitHub MCP integration requires a Personal Access Token (PAT) and cannot use the built-in `GITHUB_TOKEN`.
```yaml ```yaml
steps: steps:
- name: AI Inference with GitHub Tools - name: AI Inference with GitHub Tools
@@ -209,7 +212,7 @@ the action:
| `endpoint` | The endpoint to use for inference. If you're running this as part of an org, you should probably use the org-specific Models endpoint | `https://models.github.ai/inference` | | `endpoint` | The endpoint to use for inference. If you're running this as part of an org, you should probably use the org-specific Models endpoint | `https://models.github.ai/inference` |
| `max-tokens` | The max number of tokens to generate | 200 | | `max-tokens` | The max number of tokens to generate | 200 |
| `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` | | `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
| `github-mcp-token` | Token to use for GitHub MCP server (defaults to the main token if not specified). Use a separate PAT for tighter security | `""` | | `github-mcp-token` | Token to use for GitHub MCP server (defaults to the main token if not specified). This must be a PAT in order for MCP to work | `""` |
## Outputs ## Outputs

View File

@@ -106,6 +106,8 @@ describe('helpers.ts - inference request building', () => {
undefined, undefined,
undefined, undefined,
'gpt-4', 'gpt-4',
undefined,
undefined,
100, 100,
'https://api.test.com', 'https://api.test.com',
'test-token', 'test-token',
@@ -117,6 +119,8 @@ describe('helpers.ts - inference request building', () => {
{role: 'user', content: 'User message'}, {role: 'user', content: 'User message'},
], ],
modelName: 'gpt-4', modelName: 'gpt-4',
temperature: undefined,
topP: undefined,
maxTokens: 100, maxTokens: 100,
endpoint: 'https://api.test.com', endpoint: 'https://api.test.com',
token: 'test-token', token: 'test-token',
@@ -136,6 +140,8 @@ describe('helpers.ts - inference request building', () => {
'System prompt', 'System prompt',
'User prompt', 'User prompt',
'gpt-4', 'gpt-4',
undefined,
undefined,
100, 100,
'https://api.test.com', 'https://api.test.com',
'test-token', 'test-token',
@@ -147,6 +153,8 @@ describe('helpers.ts - inference request building', () => {
{role: 'user', content: 'User prompt'}, {role: 'user', content: 'User prompt'},
], ],
modelName: 'gpt-4', modelName: 'gpt-4',
temperature: undefined,
topP: undefined,
maxTokens: 100, maxTokens: 100,
endpoint: 'https://api.test.com', endpoint: 'https://api.test.com',
token: 'test-token', token: 'test-token',

View File

@@ -55,7 +55,7 @@ inputs:
required: false required: false
default: 'false' default: 'false'
github-mcp-token: github-mcp-token:
description: The token to use for GitHub MCP server (defaults to GITHUB_TOKEN if not specified) description: The token to use for GitHub MCP server (defaults to the main token if not specified). This must be a PAT for MCP to work.
required: false required: false
default: '' default: ''

89
dist/index.js generated vendored
View File

@@ -49496,28 +49496,18 @@ async function simpleInference(request) {
messages: request.messages, messages: request.messages,
max_tokens: request.maxTokens, max_tokens: request.maxTokens,
model: request.modelName, model: request.modelName,
temperature: request.temperature,
top_p: request.topP,
}; };
// Add response format if specified // Add response format if specified
if (request.responseFormat) { if (request.responseFormat) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
chatCompletionRequest.response_format = request.responseFormat; chatCompletionRequest.response_format = request.responseFormat;
} }
try { const response = await chatCompletion(client, chatCompletionRequest, 'simpleInference');
const response = await client.chat.completions.create(chatCompletionRequest); const modelResponse = response.choices[0]?.message?.content;
if ('choices' in response) { coreExports.info(`Model response: ${modelResponse || 'No response content'}`);
const modelResponse = response.choices[0]?.message?.content; return modelResponse || null;
coreExports.info(`Model response: ${modelResponse || 'No response content'}`);
return modelResponse || null;
}
else {
coreExports.error(`Unexpected response format from API: ${JSON.stringify(response)}`);
return null;
}
}
catch (error) {
coreExports.error(`API error: ${error}`);
throw error;
}
} }
/** /**
* GitHub MCP-enabled inference with tool execution loop * GitHub MCP-enabled inference with tool execution loop
@@ -49542,6 +49532,8 @@ async function mcpInference(request, githubMcpClient) {
messages: messages, messages: messages,
max_tokens: request.maxTokens, max_tokens: request.maxTokens,
model: request.modelName, model: request.modelName,
temperature: request.temperature,
top_p: request.topP,
}; };
// Add response format if specified (only on final iteration to avoid conflicts with tool calls) // Add response format if specified (only on final iteration to avoid conflicts with tool calls)
if (finalMessage && request.responseFormat) { if (finalMessage && request.responseFormat) {
@@ -49552,10 +49544,7 @@ async function mcpInference(request, githubMcpClient) {
chatCompletionRequest.tools = githubMcpClient.tools; chatCompletionRequest.tools = githubMcpClient.tools;
} }
try { try {
const response = await client.chat.completions.create(chatCompletionRequest); const response = await chatCompletion(client, chatCompletionRequest, `mcpInference iteration ${iterationCount}`);
if (!('choices' in response)) {
throw new Error(`Unexpected response format from API: ${JSON.stringify(response)}`);
}
const assistantMessage = response.choices[0]?.message; const assistantMessage = response.choices[0]?.message;
const modelResponse = assistantMessage?.content; const modelResponse = assistantMessage?.content;
const toolCalls = assistantMessage?.tool_calls; const toolCalls = assistantMessage?.tool_calls;
@@ -49567,17 +49556,13 @@ async function mcpInference(request, githubMcpClient) {
}); });
if (!toolCalls || toolCalls.length === 0) { if (!toolCalls || toolCalls.length === 0) {
coreExports.info('No tool calls requested, ending GitHub MCP inference loop'); coreExports.info('No tool calls requested, ending GitHub MCP inference loop');
// If we have a response format set and we haven't explicitly run one final message iteration,
// do another loop with the response format set
if (request.responseFormat && !finalMessage) { if (request.responseFormat && !finalMessage) {
coreExports.info('Making one more MCP loop with the requested response format...'); coreExports.info('Making one more MCP loop with the requested response format...');
// Add a user message requesting JSON format and try again
messages.push({ messages.push({
role: 'user', role: 'user',
content: `Please provide your response in the exact ${request.responseFormat.type} format specified.`, content: `Please provide your response in the exact ${request.responseFormat.type} format specified.`,
}); });
finalMessage = true; finalMessage = true;
// Continue the loop to get a properly formatted response
continue; continue;
} }
else { else {
@@ -49585,9 +49570,7 @@ async function mcpInference(request, githubMcpClient) {
} }
} }
coreExports.info(`Model requested ${toolCalls.length} tool calls`); coreExports.info(`Model requested ${toolCalls.length} tool calls`);
// Execute all tool calls via GitHub MCP
const toolResults = await executeToolCalls(githubMcpClient.client, toolCalls); const toolResults = await executeToolCalls(githubMcpClient.client, toolCalls);
// Add tool results to the conversation
messages.push(...toolResults); messages.push(...toolResults);
coreExports.info('Tool results added, continuing conversation...'); coreExports.info('Tool results added, continuing conversation...');
} }
@@ -49604,6 +49587,38 @@ async function mcpInference(request, githubMcpClient) {
.find(msg => msg.role === 'assistant'); .find(msg => msg.role === 'assistant');
return lastAssistantMessage?.content || null; return lastAssistantMessage?.content || null;
} }
/**
* Wrapper around OpenAI chat.completions.create with defensive handling for cases where
* the SDK returns a raw string (e.g., unexpected content-type or streaming body) instead of
* a parsed object. Ensures an object with a 'choices' array is returned or throws a descriptive error.
*/
async function chatCompletion(client, params, context) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let response = await client.chat.completions.create(params);
coreExports.debug(`${context}: raw response typeof=${typeof response}`);
if (typeof response === 'string') {
// Attempt to parse if we unexpectedly received a string
try {
response = JSON.parse(response);
}
catch (e) {
const preview = response.slice(0, 400);
throw new Error(`${context}: Chat completion response was a string and not valid JSON (${e.message}). Preview: ${preview}`);
}
}
if (!response || typeof response !== 'object' || !('choices' in response)) {
const preview = JSON.stringify(response)?.slice(0, 800);
throw new Error(`${context}: Unexpected response shape (no choices). Preview: ${preview}`);
}
return response;
}
catch (err) {
// Re-throw after logging for upstream handling
coreExports.error(`${context}: chatCompletion failed: ${err}`);
throw err;
}
}
/** /**
* Helper function to load content from a file or use fallback input * Helper function to load content from a file or use fallback input
@@ -49674,12 +49689,14 @@ function buildResponseFormat(promptConfig) {
/** /**
* Build complete InferenceRequest from prompt config and inputs * Build complete InferenceRequest from prompt config and inputs
*/ */
function buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, maxTokens, endpoint, token) { function buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, temperature, topP, maxTokens, endpoint, token) {
const messages = buildMessages(promptConfig, systemPrompt, prompt); const messages = buildMessages(promptConfig, systemPrompt, prompt);
const responseFormat = buildResponseFormat(promptConfig); const responseFormat = buildResponseFormat(promptConfig);
return { return {
messages, messages,
modelName, modelName,
temperature,
topP,
maxTokens, maxTokens,
endpoint, endpoint,
token, token,
@@ -52561,10 +52578,8 @@ function loadPromptFile(filePath, templateVariables = {}) {
throw new Error(`Prompt file not found: ${filePath}`); throw new Error(`Prompt file not found: ${filePath}`);
} }
const fileContent = fs.readFileSync(filePath, 'utf-8'); const fileContent = fs.readFileSync(filePath, 'utf-8');
// Apply template variable substitution
const processedContent = replaceTemplateVariables(fileContent, templateVariables);
try { try {
const config = load(processedContent); const config = load(fileContent);
if (!config.messages || !Array.isArray(config.messages)) { if (!config.messages || !Array.isArray(config.messages)) {
throw new Error('Prompt file must contain a "messages" array'); throw new Error('Prompt file must contain a "messages" array');
} }
@@ -52577,6 +52592,13 @@ function loadPromptFile(filePath, templateVariables = {}) {
throw new Error(`Invalid message role: ${message.role}`); throw new Error(`Invalid message role: ${message.role}`);
} }
} }
// Prepare messages by replacing template variables with actual content
config.messages = config.messages.map(msg => {
return {
...msg,
content: replaceTemplateVariables(msg.content, templateVariables),
};
});
return config; return config;
} }
catch (error) { catch (error) {
@@ -52624,7 +52646,10 @@ async function run() {
} }
// Get common parameters // Get common parameters
const modelName = promptConfig?.model || coreExports.getInput('model'); const modelName = promptConfig?.model || coreExports.getInput('model');
const maxTokens = parseInt(coreExports.getInput('max-tokens'), 10); let maxTokens = promptConfig?.modelParameters?.maxTokens ?? coreExports.getInput('max-tokens');
if (typeof maxTokens === 'string') {
maxTokens = parseInt(maxTokens, 10);
}
const token = process.env['GITHUB_TOKEN'] || coreExports.getInput('token'); const token = process.env['GITHUB_TOKEN'] || coreExports.getInput('token');
if (token === undefined) { if (token === undefined) {
throw new Error('GITHUB_TOKEN is not set'); throw new Error('GITHUB_TOKEN is not set');
@@ -52633,7 +52658,7 @@ async function run() {
const githubMcpToken = coreExports.getInput('github-mcp-token') || token; const githubMcpToken = coreExports.getInput('github-mcp-token') || token;
const endpoint = coreExports.getInput('endpoint'); const endpoint = coreExports.getInput('endpoint');
// Build the inference request with pre-processed messages and response format // Build the inference request with pre-processed messages and response format
const inferenceRequest = buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, maxTokens, endpoint, token); const inferenceRequest = buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, promptConfig?.modelParameters?.temperature, promptConfig?.modelParameters?.topP, maxTokens, endpoint, token);
const enableMcp = coreExports.getBooleanInput('enable-github-mcp') || false; const enableMcp = coreExports.getBooleanInput('enable-github-mcp') || false;
let modelResponse = null; let modelResponse = null;
if (enableMcp) { if (enableMcp) {

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -82,6 +82,8 @@ export function buildInferenceRequest(
systemPrompt: string | undefined, systemPrompt: string | undefined,
prompt: string | undefined, prompt: string | undefined,
modelName: string, modelName: string,
temperature: number | undefined,
topP: number | undefined,
maxTokens: number, maxTokens: number,
endpoint: string, endpoint: string,
token: string, token: string,
@@ -92,6 +94,8 @@ export function buildInferenceRequest(
return { return {
messages, messages,
modelName, modelName,
temperature,
topP,
maxTokens, maxTokens,
endpoint, endpoint,
token, token,

View File

@@ -15,6 +15,8 @@ export interface InferenceRequest {
maxTokens: number maxTokens: number
endpoint: string endpoint: string
token: string token: string
temperature?: number
topP?: number
responseFormat?: {type: 'json_schema'; json_schema: unknown} // Processed response format for the API responseFormat?: {type: 'json_schema'; json_schema: unknown} // Processed response format for the API
} }
@@ -45,6 +47,8 @@ export async function simpleInference(request: InferenceRequest): Promise<string
messages: request.messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[], messages: request.messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
max_tokens: request.maxTokens, max_tokens: request.maxTokens,
model: request.modelName, model: request.modelName,
temperature: request.temperature,
top_p: request.topP,
} }
// Add response format if specified // Add response format if specified
@@ -53,21 +57,10 @@ export async function simpleInference(request: InferenceRequest): Promise<string
chatCompletionRequest.response_format = request.responseFormat as any chatCompletionRequest.response_format = request.responseFormat as any
} }
try { const response = await chatCompletion(client, chatCompletionRequest, 'simpleInference')
const response = await client.chat.completions.create(chatCompletionRequest) const modelResponse = response.choices[0]?.message?.content
core.info(`Model response: ${modelResponse || 'No response content'}`)
if ('choices' in response) { return modelResponse || null
const modelResponse = response.choices[0]?.message?.content
core.info(`Model response: ${modelResponse || 'No response content'}`)
return modelResponse || null
} else {
core.error(`Unexpected response format from API: ${JSON.stringify(response)}`)
return null
}
} catch (error) {
core.error(`API error: ${error}`)
throw error
}
} }
/** /**
@@ -101,6 +94,8 @@ export async function mcpInference(
messages: messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[], messages: messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
max_tokens: request.maxTokens, max_tokens: request.maxTokens,
model: request.modelName, model: request.modelName,
temperature: request.temperature,
top_p: request.topP,
} }
// Add response format if specified (only on final iteration to avoid conflicts with tool calls) // Add response format if specified (only on final iteration to avoid conflicts with tool calls)
@@ -112,11 +107,7 @@ export async function mcpInference(
} }
try { try {
const response = await client.chat.completions.create(chatCompletionRequest) const response = await chatCompletion(client, chatCompletionRequest, `mcpInference iteration ${iterationCount}`)
if (!('choices' in response)) {
throw new Error(`Unexpected response format from API: ${JSON.stringify(response)}`)
}
const assistantMessage = response.choices[0]?.message const assistantMessage = response.choices[0]?.message
const modelResponse = assistantMessage?.content const modelResponse = assistantMessage?.content
@@ -133,20 +124,13 @@ export async function mcpInference(
if (!toolCalls || toolCalls.length === 0) { if (!toolCalls || toolCalls.length === 0) {
core.info('No tool calls requested, ending GitHub MCP inference loop') core.info('No tool calls requested, ending GitHub MCP inference loop')
// If we have a response format set and we haven't explicitly run one final message iteration,
// do another loop with the response format set
if (request.responseFormat && !finalMessage) { if (request.responseFormat && !finalMessage) {
core.info('Making one more MCP loop with the requested response format...') core.info('Making one more MCP loop with the requested response format...')
// Add a user message requesting JSON format and try again
messages.push({ messages.push({
role: 'user', role: 'user',
content: `Please provide your response in the exact ${request.responseFormat.type} format specified.`, content: `Please provide your response in the exact ${request.responseFormat.type} format specified.`,
}) })
finalMessage = true finalMessage = true
// Continue the loop to get a properly formatted response
continue continue
} else { } else {
return modelResponse || null return modelResponse || null
@@ -154,13 +138,8 @@ export async function mcpInference(
} }
core.info(`Model requested ${toolCalls.length} tool calls`) core.info(`Model requested ${toolCalls.length} tool calls`)
// Execute all tool calls via GitHub MCP
const toolResults = await executeToolCalls(githubMcpClient.client, toolCalls as ToolCall[]) const toolResults = await executeToolCalls(githubMcpClient.client, toolCalls as ToolCall[])
// Add tool results to the conversation
messages.push(...toolResults) messages.push(...toolResults)
core.info('Tool results added, continuing conversation...') core.info('Tool results added, continuing conversation...')
} catch (error) { } catch (error) {
core.error(`OpenAI API error: ${error}`) core.error(`OpenAI API error: ${error}`)
@@ -178,3 +157,43 @@ export async function mcpInference(
return lastAssistantMessage?.content || null return lastAssistantMessage?.content || null
} }
/**
* Wrapper around OpenAI chat.completions.create with defensive handling for cases where
* the SDK returns a raw string (e.g., unexpected content-type or streaming body) instead of
* a parsed object. Ensures an object with a 'choices' array is returned or throws a descriptive error.
*/
async function chatCompletion(
client: OpenAI,
params: OpenAI.Chat.Completions.ChatCompletionCreateParams,
context: string,
): Promise<OpenAI.Chat.Completions.ChatCompletion> {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let response: any = await client.chat.completions.create(params)
core.debug(`${context}: raw response typeof=${typeof response}`)
if (typeof response === 'string') {
// Attempt to parse if we unexpectedly received a string
try {
response = JSON.parse(response)
} catch (e) {
const preview = response.slice(0, 400)
throw new Error(
`${context}: Chat completion response was a string and not valid JSON (${(e as Error).message}). Preview: ${preview}`,
)
}
}
if (!response || typeof response !== 'object' || !('choices' in response)) {
const preview = JSON.stringify(response)?.slice(0, 800)
throw new Error(`${context}: Unexpected response shape (no choices). Preview: ${preview}`)
}
return response as OpenAI.Chat.Completions.ChatCompletion
} catch (err) {
// Re-throw after logging for upstream handling
core.error(`${context}: chatCompletion failed: ${err}`)
throw err
}
}

View File

@@ -53,7 +53,11 @@ export async function run(): Promise<void> {
// Get common parameters // Get common parameters
const modelName = promptConfig?.model || core.getInput('model') const modelName = promptConfig?.model || core.getInput('model')
const maxTokens = parseInt(core.getInput('max-tokens'), 10) let maxTokens = promptConfig?.modelParameters?.maxTokens ?? core.getInput('max-tokens')
if (typeof maxTokens === 'string') {
maxTokens = parseInt(maxTokens, 10)
}
const token = process.env['GITHUB_TOKEN'] || core.getInput('token') const token = process.env['GITHUB_TOKEN'] || core.getInput('token')
if (token === undefined) { if (token === undefined) {
@@ -71,6 +75,8 @@ export async function run(): Promise<void> {
systemPrompt, systemPrompt,
prompt, prompt,
modelName, modelName,
promptConfig?.modelParameters?.temperature,
promptConfig?.modelParameters?.topP,
maxTokens, maxTokens,
endpoint, endpoint,
token, token,

View File

@@ -7,9 +7,16 @@ export interface PromptMessage {
content: string content: string
} }
export interface ModelParameters {
maxTokens?: number
temperature?: number
topP?: number
}
export interface PromptConfig { export interface PromptConfig {
messages: PromptMessage[] messages: PromptMessage[]
model?: string model?: string
modelParameters?: ModelParameters
responseFormat?: 'text' | 'json_schema' responseFormat?: 'text' | 'json_schema'
jsonSchema?: string jsonSchema?: string
} }
@@ -101,11 +108,8 @@ export function loadPromptFile(filePath: string, templateVariables: TemplateVari
const fileContent = fs.readFileSync(filePath, 'utf-8') const fileContent = fs.readFileSync(filePath, 'utf-8')
// Apply template variable substitution
const processedContent = replaceTemplateVariables(fileContent, templateVariables)
try { try {
const config = yaml.load(processedContent) as PromptConfig const config = yaml.load(fileContent) as PromptConfig
if (!config.messages || !Array.isArray(config.messages)) { if (!config.messages || !Array.isArray(config.messages)) {
throw new Error('Prompt file must contain a "messages" array') throw new Error('Prompt file must contain a "messages" array')
@@ -121,6 +125,14 @@ export function loadPromptFile(filePath: string, templateVariables: TemplateVari
} }
} }
// Prepare messages by replacing template variables with actual content
config.messages = config.messages.map(msg => {
return {
...msg,
content: replaceTemplateVariables(msg.content, templateVariables),
}
})
return config return config
} catch (error) { } catch (error) {
throw new Error(`Failed to parse prompt file: ${error instanceof Error ? error.message : 'Unknown error'}`) throw new Error(`Failed to parse prompt file: ${error instanceof Error ? error.message : 'Unknown error'}`)