Merge pull request #163 from yg1996/add-custom-headers-support
Add custom headers support for API Management integration
This commit is contained in:
@@ -156,6 +156,54 @@ steps:
|
|||||||
cat "${{ steps.inference.outputs.response-file }}"
|
cat "${{ steps.inference.outputs.response-file }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Using custom headers
|
||||||
|
|
||||||
|
You can include custom HTTP headers in your API requests, which is useful for integrating with API Management platforms, adding tracking information, or routing requests through custom gateways.
|
||||||
|
|
||||||
|
#### YAML format (recommended for multiple headers)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- name: AI Inference with Azure APIM
|
||||||
|
id: inference
|
||||||
|
uses: actions/ai-inference@v1
|
||||||
|
with:
|
||||||
|
prompt: 'Analyze this code for security issues...'
|
||||||
|
endpoint: ${{ secrets.APIM_ENDPOINT }}
|
||||||
|
token: ${{ secrets.APIM_KEY }}
|
||||||
|
custom-headers: |
|
||||||
|
Ocp-Apim-Subscription-Key: ${{ secrets.APIM_SUBSCRIPTION_KEY }}
|
||||||
|
serviceName: code-review-workflow
|
||||||
|
env: production
|
||||||
|
team: security
|
||||||
|
computer: github-actions
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JSON format (alternative for compact syntax)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- name: AI Inference with Custom Headers
|
||||||
|
id: inference
|
||||||
|
uses: actions/ai-inference@v1
|
||||||
|
with:
|
||||||
|
prompt: 'Hello!'
|
||||||
|
custom-headers: '{"X-Custom-Header": "value", "X-Team": "engineering", "X-Request-ID": "${{ github.run_id }}"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Use cases for custom headers
|
||||||
|
|
||||||
|
- **API Management**: Integrate with Azure APIM, AWS API Gateway, Kong, or other API management platforms
|
||||||
|
- **Request tracking**: Add correlation IDs, request IDs, or workflow identifiers
|
||||||
|
- **Rate limiting**: Include quota or tier information for custom rate limiting
|
||||||
|
- **Multi-tenancy**: Identify teams, services, or environments
|
||||||
|
- **Observability**: Add metadata for logging, monitoring, and debugging
|
||||||
|
- **Routing**: Control request routing through custom gateways or load balancers
|
||||||
|
|
||||||
|
**Header name requirements**: Header names must follow the HTTP token syntax defined in RFC 7230 (which permits underscores). For maximum compatibility with intermediaries and tooling, we recommend using only alphanumeric characters and hyphens.
|
||||||
|
|
||||||
|
**Security note**: Always use GitHub secrets for sensitive header values like API keys, tokens, or passwords. The action automatically masks common sensitive headers (containing `key`, `token`, `secret`, `password`, or `authorization`) in logs.
|
||||||
|
|
||||||
### GitHub MCP Integration (Model Context Protocol)
|
### GitHub MCP Integration (Model Context Protocol)
|
||||||
|
|
||||||
This action now supports **read-only** integration with the GitHub-hosted Model
|
This action now supports **read-only** integration with the GitHub-hosted Model
|
||||||
@@ -228,20 +276,21 @@ perform actions like searching issues and PRs.
|
|||||||
Various inputs are defined in [`action.yml`](action.yml) to let you configure
|
Various inputs are defined in [`action.yml`](action.yml) to let you configure
|
||||||
the action:
|
the action:
|
||||||
|
|
||||||
| Name | Description | Default |
|
| Name | Description | Default |
|
||||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ |
|
||||||
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
|
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
|
||||||
| `prompt` | The prompt to send to the model | N/A |
|
| `prompt` | The prompt to send to the model | N/A |
|
||||||
| `prompt-file` | Path to a file containing the prompt (supports .txt and .prompt.yml formats). If both `prompt` and `prompt-file` are provided, `prompt-file` takes precedence | `""` |
|
| `prompt-file` | Path to a file containing the prompt (supports .txt and .prompt.yml formats). If both `prompt` and `prompt-file` are provided, `prompt-file` takes precedence | `""` |
|
||||||
| `input` | Template variables in YAML format for .prompt.yml files (e.g., `var1: value1` on separate lines) | `""` |
|
| `input` | Template variables in YAML format for .prompt.yml files (e.g., `var1: value1` on separate lines) | `""` |
|
||||||
| `file_input` | Template variables in YAML where values are file paths. The file contents are read and used for templating | `""` |
|
| `file_input` | Template variables in YAML where values are file paths. The file contents are read and used for templating | `""` |
|
||||||
| `system-prompt` | The system prompt to send to the model | `"You are a helpful assistant"` |
|
| `system-prompt` | The system prompt to send to the model | `"You are a helpful assistant"` |
|
||||||
| `system-prompt-file` | Path to a file containing the system prompt. If both `system-prompt` and `system-prompt-file` are provided, `system-prompt-file` takes precedence | `""` |
|
| `system-prompt-file` | Path to a file containing the system prompt. If both `system-prompt` and `system-prompt-file` are provided, `system-prompt-file` takes precedence | `""` |
|
||||||
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `openai/gpt-4o` |
|
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `openai/gpt-4o` |
|
||||||
| `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). | `""` |
|
| `github-mcp-token` | Token to use for GitHub MCP server (defaults to the main token if not specified). | `""` |
|
||||||
|
| `custom-headers` | Custom HTTP headers to include in API requests. Supports both YAML format (`header1: value1`) and JSON format (`{"header1": "value1"}`). Useful for API Management platforms, rate limiting, and request tracking. | `""` |
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
|
|
||||||
|
|||||||
+240
-1
@@ -11,7 +11,7 @@ vi.mock('fs', () => ({
|
|||||||
|
|
||||||
vi.mock('@actions/core', () => core)
|
vi.mock('@actions/core', () => core)
|
||||||
|
|
||||||
const {loadContentFromFileOrInput} = await import('../src/helpers.js')
|
const {loadContentFromFileOrInput, parseCustomHeaders} = await import('../src/helpers.js')
|
||||||
|
|
||||||
describe('helpers.ts', () => {
|
describe('helpers.ts', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -132,4 +132,243 @@ describe('helpers.ts', () => {
|
|||||||
expect(result).toBe(defaultValue)
|
expect(result).toBe(defaultValue)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('parseCustomHeaders', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses YAML format headers correctly', () => {
|
||||||
|
const yamlInput = `header1: value1
|
||||||
|
header2: value2
|
||||||
|
X-Custom-Header: custom-value`
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(yamlInput)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
header1: 'value1',
|
||||||
|
header2: 'value2',
|
||||||
|
'X-Custom-Header': 'custom-value',
|
||||||
|
})
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Custom header added: header1: value1')
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Custom header added: header2: value2')
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Custom header added: X-Custom-Header: custom-value')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses JSON format headers correctly', () => {
|
||||||
|
const jsonInput = '{"header1": "value1", "header2": "value2", "X-Team": "engineering"}'
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(jsonInput)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
header1: 'value1',
|
||||||
|
header2: 'value2',
|
||||||
|
'X-Team': 'engineering',
|
||||||
|
})
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Custom header added: header1: value1')
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Custom header added: header2: value2')
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Custom header added: X-Team: engineering')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns empty object for empty input', () => {
|
||||||
|
expect(parseCustomHeaders('')).toEqual({})
|
||||||
|
expect(parseCustomHeaders(' ')).toEqual({})
|
||||||
|
expect(core.warning).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('masks sensitive header values in logs', () => {
|
||||||
|
const yamlInput = `Ocp-Apim-Subscription-Key: secret123
|
||||||
|
X-Api-Token: token456
|
||||||
|
Authorization: Bearer abc123
|
||||||
|
serviceName: my-service
|
||||||
|
password: pass123`
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(yamlInput)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
'Ocp-Apim-Subscription-Key': 'secret123',
|
||||||
|
'X-Api-Token': 'token456',
|
||||||
|
Authorization: 'Bearer abc123',
|
||||||
|
serviceName: 'my-service',
|
||||||
|
password: 'pass123',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sensitive headers should be masked
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Custom header added: Ocp-Apim-Subscription-Key: ***MASKED***')
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Custom header added: X-Api-Token: ***MASKED***')
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Custom header added: Authorization: ***MASKED***')
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Custom header added: password: ***MASKED***')
|
||||||
|
|
||||||
|
// Non-sensitive headers should not be masked
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Custom header added: serviceName: my-service')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('validates header names and skips invalid ones', () => {
|
||||||
|
const yamlInput = `valid-header: value1
|
||||||
|
invalid header: value2
|
||||||
|
invalid_underscore: value3
|
||||||
|
invalid@header: value4
|
||||||
|
valid123: value5`
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(yamlInput)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
'valid-header': 'value1',
|
||||||
|
valid123: 'value5',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining('Skipping invalid header name: invalid header'))
|
||||||
|
expect(core.warning).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Skipping invalid header name: invalid_underscore'),
|
||||||
|
)
|
||||||
|
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining('Skipping invalid header name: invalid@header'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('warns and returns empty object for invalid JSON', () => {
|
||||||
|
const invalidJson = '{invalid json}'
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(invalidJson)
|
||||||
|
|
||||||
|
expect(result).toEqual({})
|
||||||
|
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining('Failed to parse custom headers'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('warns and returns empty object for invalid YAML', () => {
|
||||||
|
const invalidYaml = 'invalid: yaml: structure: bad'
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(invalidYaml)
|
||||||
|
|
||||||
|
expect(result).toEqual({})
|
||||||
|
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining('Failed to parse custom headers'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('warns and returns empty object for JSON array', () => {
|
||||||
|
const jsonArray = '["header1", "header2"]'
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(jsonArray)
|
||||||
|
|
||||||
|
expect(result).toEqual({})
|
||||||
|
expect(core.warning).toHaveBeenCalledWith('Custom headers JSON must be an object, not null or an array')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('warns and returns empty object for null value', () => {
|
||||||
|
// The string 'null' is valid YAML and gets parsed as null
|
||||||
|
const nullValue = 'null'
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(nullValue)
|
||||||
|
|
||||||
|
expect(result).toEqual({})
|
||||||
|
expect(core.warning).toHaveBeenCalledWith('Custom headers YAML must be an object')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('warns and returns empty object for YAML array', () => {
|
||||||
|
const yamlArray = `- header1
|
||||||
|
- header2`
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(yamlArray)
|
||||||
|
|
||||||
|
expect(result).toEqual({})
|
||||||
|
expect(core.warning).toHaveBeenCalledWith('Custom headers YAML must be an object')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('converts non-string values to strings', () => {
|
||||||
|
const jsonInput = '{"numericHeader": 123, "boolHeader": true, "nullHeader": null}'
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(jsonInput)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
numericHeader: '123',
|
||||||
|
boolHeader: 'true',
|
||||||
|
nullHeader: 'null',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects header values with newline characters (LF)', () => {
|
||||||
|
const jsonInput = '{"X-Custom-Header": "value\\nwith\\nnewline", "header1": "safe-value"}'
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(jsonInput)
|
||||||
|
|
||||||
|
// Only the safe header should be accepted
|
||||||
|
expect(result).toEqual({
|
||||||
|
header1: 'safe-value',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(core.warning).toHaveBeenCalledWith(
|
||||||
|
'Skipping header "X-Custom-Header" because its value contains newline characters, which are not allowed in HTTP header values.',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects header values with carriage return characters (CR)', () => {
|
||||||
|
const jsonInput = '{"X-Injected": "value\\rwith\\rcarriage", "X-Safe": "safe-value"}'
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(jsonInput)
|
||||||
|
|
||||||
|
// Only the safe header should be accepted
|
||||||
|
expect(result).toEqual({
|
||||||
|
'X-Safe': 'safe-value',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(core.warning).toHaveBeenCalledWith(
|
||||||
|
'Skipping header "X-Injected" because its value contains newline characters, which are not allowed in HTTP header values.',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects header values with CRLF sequences', () => {
|
||||||
|
const jsonInput = '{"X-Attack": "value\\r\\nInjected-Header: malicious", "X-Valid": "normal"}'
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(jsonInput)
|
||||||
|
|
||||||
|
// Only the valid header should be accepted
|
||||||
|
expect(result).toEqual({
|
||||||
|
'X-Valid': 'normal',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(core.warning).toHaveBeenCalledWith(
|
||||||
|
'Skipping header "X-Attack" because its value contains newline characters, which are not allowed in HTTP header values.',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects multiline YAML values for security', () => {
|
||||||
|
const yamlInput = `header1: value1
|
||||||
|
header2: |
|
||||||
|
multiline
|
||||||
|
value
|
||||||
|
here`
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(yamlInput)
|
||||||
|
|
||||||
|
// header2 should be rejected because it contains newlines
|
||||||
|
expect(result).toEqual({
|
||||||
|
header1: 'value1',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(core.warning).toHaveBeenCalledWith(
|
||||||
|
'Skipping header "header2" because its value contains newline characters, which are not allowed in HTTP header values.',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles complex real-world Azure APIM example', () => {
|
||||||
|
const apimHeaders = `Ocp-Apim-Subscription-Key: my-subscription-key-123
|
||||||
|
serviceName: terraform-plan-workflow
|
||||||
|
env: prod
|
||||||
|
team: infrastructure
|
||||||
|
computer: github-actions
|
||||||
|
systemID: terraform-ci`
|
||||||
|
|
||||||
|
const result = parseCustomHeaders(apimHeaders)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
'Ocp-Apim-Subscription-Key': 'my-subscription-key-123',
|
||||||
|
serviceName: 'terraform-plan-workflow',
|
||||||
|
env: 'prod',
|
||||||
|
team: 'infrastructure',
|
||||||
|
computer: 'github-actions',
|
||||||
|
systemID: 'terraform-ci',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Only the subscription key should be masked
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Custom header added: Ocp-Apim-Subscription-Key: ***MASKED***')
|
||||||
|
expect(core.info).toHaveBeenCalledWith('Custom header added: serviceName: terraform-plan-workflow')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -75,6 +75,49 @@ describe('inference.ts', () => {
|
|||||||
max_tokens: 100,
|
max_tokens: 100,
|
||||||
model: 'gpt-4',
|
model: 'gpt-4',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Verify OpenAI client was initialized with empty custom headers
|
||||||
|
expect(mockOpenAIClient).toHaveBeenCalledWith({
|
||||||
|
apiKey: 'test-token',
|
||||||
|
baseURL: 'https://api.test.com',
|
||||||
|
defaultHeaders: {},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('includes custom headers in OpenAI client', async () => {
|
||||||
|
const requestWithHeaders = {
|
||||||
|
...mockRequest,
|
||||||
|
customHeaders: {
|
||||||
|
'X-Custom-Header': 'custom-value',
|
||||||
|
'Ocp-Apim-Subscription-Key': 'secret123',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
content: 'Response with headers',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
mockCreate.mockResolvedValue(mockResponse)
|
||||||
|
|
||||||
|
const result = await simpleInference(requestWithHeaders)
|
||||||
|
|
||||||
|
expect(result).toBe('Response with headers')
|
||||||
|
|
||||||
|
// Verify OpenAI client was initialized with custom headers
|
||||||
|
expect(mockOpenAIClient).toHaveBeenCalledWith({
|
||||||
|
apiKey: 'test-token',
|
||||||
|
baseURL: 'https://api.test.com',
|
||||||
|
defaultHeaders: {
|
||||||
|
'X-Custom-Header': 'custom-value',
|
||||||
|
'Ocp-Apim-Subscription-Key': 'secret123',
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('handles null response content', async () => {
|
it('handles null response content', async () => {
|
||||||
@@ -186,6 +229,50 @@ describe('inference.ts', () => {
|
|||||||
expect(callArgs.response_format).toBeUndefined()
|
expect(callArgs.response_format).toBeUndefined()
|
||||||
expect(callArgs.model).toBe('gpt-4')
|
expect(callArgs.model).toBe('gpt-4')
|
||||||
expect(callArgs.max_tokens).toBe(100)
|
expect(callArgs.max_tokens).toBe(100)
|
||||||
|
|
||||||
|
// Verify OpenAI client was initialized with empty custom headers
|
||||||
|
expect(mockOpenAIClient).toHaveBeenCalledWith({
|
||||||
|
apiKey: 'test-token',
|
||||||
|
baseURL: 'https://api.test.com',
|
||||||
|
defaultHeaders: {},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('includes custom headers in MCP inference', async () => {
|
||||||
|
const requestWithHeaders = {
|
||||||
|
...mockRequest,
|
||||||
|
customHeaders: {
|
||||||
|
serviceName: 'test-service',
|
||||||
|
'X-Team': 'engineering',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
content: 'MCP response with headers',
|
||||||
|
tool_calls: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
mockCreate.mockResolvedValue(mockResponse)
|
||||||
|
|
||||||
|
const result = await mcpInference(requestWithHeaders, mockMcpClient)
|
||||||
|
|
||||||
|
expect(result).toBe('MCP response with headers')
|
||||||
|
|
||||||
|
// Verify OpenAI client was initialized with custom headers
|
||||||
|
expect(mockOpenAIClient).toHaveBeenCalledWith({
|
||||||
|
apiKey: 'test-token',
|
||||||
|
baseURL: 'https://api.test.com',
|
||||||
|
defaultHeaders: {
|
||||||
|
serviceName: 'test-service',
|
||||||
|
'X-Team': 'engineering',
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('executes tool calls and continues conversation', async () => {
|
it('executes tool calls and continues conversation', async () => {
|
||||||
|
|||||||
@@ -171,6 +171,9 @@ describe('main.ts', () => {
|
|||||||
endpoint: 'https://api.test.com',
|
endpoint: 'https://api.test.com',
|
||||||
token: 'fake-token',
|
token: 'fake-token',
|
||||||
responseFormat: undefined,
|
responseFormat: undefined,
|
||||||
|
temperature: undefined,
|
||||||
|
topP: undefined,
|
||||||
|
customHeaders: {},
|
||||||
})
|
})
|
||||||
expect(mockConnectToGitHubMCP).not.toHaveBeenCalled()
|
expect(mockConnectToGitHubMCP).not.toHaveBeenCalled()
|
||||||
expect(mockMcpInference).not.toHaveBeenCalled()
|
expect(mockMcpInference).not.toHaveBeenCalled()
|
||||||
@@ -259,6 +262,9 @@ describe('main.ts', () => {
|
|||||||
endpoint: 'https://api.test.com',
|
endpoint: 'https://api.test.com',
|
||||||
token: 'fake-token',
|
token: 'fake-token',
|
||||||
responseFormat: undefined,
|
responseFormat: undefined,
|
||||||
|
temperature: undefined,
|
||||||
|
topP: undefined,
|
||||||
|
customHeaders: {},
|
||||||
})
|
})
|
||||||
verifyStandardResponse()
|
verifyStandardResponse()
|
||||||
expect(mockProcessExit).toHaveBeenCalledWith(0)
|
expect(mockProcessExit).toHaveBeenCalledWith(0)
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ inputs:
|
|||||||
description: 'Comma-separated list of toolsets to enable for GitHub MCP (e.g., "repos,issues,pull_requests,actions"). Use "all" for all toolsets, "default" for default set. If not specified, uses default toolsets (context,repos,issues,pull_requests,users).'
|
description: 'Comma-separated list of toolsets to enable for GitHub MCP (e.g., "repos,issues,pull_requests,actions"). Use "all" for all toolsets, "default" for default set. If not specified, uses default toolsets (context,repos,issues,pull_requests,users).'
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
|
custom-headers:
|
||||||
|
description: 'Custom HTTP headers to include in API requests. Supports both YAML format (header1: value1) and JSON format ({"header1": "value1"}). Useful for API Management platforms, rate limiting, and request tracking.'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
# Define your outputs here.
|
# Define your outputs here.
|
||||||
outputs:
|
outputs:
|
||||||
|
|||||||
+158
-85
@@ -58308,6 +58308,7 @@ async function simpleInference(request) {
|
|||||||
const client = new OpenAI({
|
const client = new OpenAI({
|
||||||
apiKey: request.token,
|
apiKey: request.token,
|
||||||
baseURL: request.endpoint,
|
baseURL: request.endpoint,
|
||||||
|
defaultHeaders: request.customHeaders || {},
|
||||||
});
|
});
|
||||||
const chatCompletionRequest = {
|
const chatCompletionRequest = {
|
||||||
messages: request.messages,
|
messages: request.messages,
|
||||||
@@ -58334,6 +58335,7 @@ async function mcpInference(request, githubMcpClient) {
|
|||||||
const client = new OpenAI({
|
const client = new OpenAI({
|
||||||
apiKey: request.token,
|
apiKey: request.token,
|
||||||
baseURL: request.endpoint,
|
baseURL: request.endpoint,
|
||||||
|
defaultHeaders: request.customHeaders || {},
|
||||||
});
|
});
|
||||||
// Start with the pre-processed messages
|
// Start with the pre-processed messages
|
||||||
const messages = [...request.messages];
|
const messages = [...request.messages];
|
||||||
@@ -58437,90 +58439,6 @@ async function chatCompletion(client, params, context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to load content from a file or use fallback input
|
|
||||||
* @param filePathInput - Input name for the file path
|
|
||||||
* @param contentInput - Input name for the direct content
|
|
||||||
* @param defaultValue - Default value to use if neither file nor content is provided
|
|
||||||
* @returns The loaded content
|
|
||||||
*/
|
|
||||||
function loadContentFromFileOrInput(filePathInput, contentInput, defaultValue) {
|
|
||||||
const filePath = coreExports.getInput(filePathInput);
|
|
||||||
const contentString = coreExports.getInput(contentInput);
|
|
||||||
if (filePath !== undefined && filePath !== '') {
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
throw new Error(`File for ${filePathInput} was not found: ${filePath}`);
|
|
||||||
}
|
|
||||||
return fs.readFileSync(filePath, 'utf-8');
|
|
||||||
}
|
|
||||||
else if (contentString !== undefined && contentString !== '') {
|
|
||||||
return contentString;
|
|
||||||
}
|
|
||||||
else if (defaultValue !== undefined) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new Error(`Neither ${filePathInput} nor ${contentInput} was set`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Build messages array from either prompt config or legacy format
|
|
||||||
*/
|
|
||||||
function buildMessages(promptConfig, systemPrompt, prompt) {
|
|
||||||
if (promptConfig?.messages && promptConfig.messages.length > 0) {
|
|
||||||
// Use new message format
|
|
||||||
return promptConfig.messages.map(msg => ({
|
|
||||||
role: msg.role,
|
|
||||||
content: msg.content,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Use legacy format
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: systemPrompt || 'You are a helpful assistant',
|
|
||||||
},
|
|
||||||
{ role: 'user', content: prompt || '' },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Build response format object for API from prompt config
|
|
||||||
*/
|
|
||||||
function buildResponseFormat(promptConfig) {
|
|
||||||
if (promptConfig?.responseFormat === 'json_schema' && promptConfig.jsonSchema) {
|
|
||||||
try {
|
|
||||||
const schema = JSON.parse(promptConfig.jsonSchema);
|
|
||||||
return {
|
|
||||||
type: 'json_schema',
|
|
||||||
json_schema: schema,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
throw new Error(`Invalid JSON schema: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Build complete InferenceRequest from prompt config and inputs
|
|
||||||
*/
|
|
||||||
function buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, temperature, topP, maxTokens, endpoint, token) {
|
|
||||||
const messages = buildMessages(promptConfig, systemPrompt, prompt);
|
|
||||||
const responseFormat = buildResponseFormat(promptConfig);
|
|
||||||
return {
|
|
||||||
messages,
|
|
||||||
modelName,
|
|
||||||
temperature,
|
|
||||||
topP,
|
|
||||||
maxTokens,
|
|
||||||
endpoint,
|
|
||||||
token,
|
|
||||||
responseFormat,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
|
/*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
|
||||||
function isNothing(subject) {
|
function isNothing(subject) {
|
||||||
return (typeof subject === 'undefined') || (subject === null);
|
return (typeof subject === 'undefined') || (subject === null);
|
||||||
@@ -61328,6 +61246,158 @@ var loader = {
|
|||||||
};
|
};
|
||||||
var load = loader.load;
|
var load = loader.load;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to load content from a file or use fallback input
|
||||||
|
* @param filePathInput - Input name for the file path
|
||||||
|
* @param contentInput - Input name for the direct content
|
||||||
|
* @param defaultValue - Default value to use if neither file nor content is provided
|
||||||
|
* @returns The loaded content
|
||||||
|
*/
|
||||||
|
function loadContentFromFileOrInput(filePathInput, contentInput, defaultValue) {
|
||||||
|
const filePath = coreExports.getInput(filePathInput);
|
||||||
|
const contentString = coreExports.getInput(contentInput);
|
||||||
|
if (filePath !== undefined && filePath !== '') {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
throw new Error(`File for ${filePathInput} was not found: ${filePath}`);
|
||||||
|
}
|
||||||
|
return fs.readFileSync(filePath, 'utf-8');
|
||||||
|
}
|
||||||
|
else if (contentString !== undefined && contentString !== '') {
|
||||||
|
return contentString;
|
||||||
|
}
|
||||||
|
else if (defaultValue !== undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`Neither ${filePathInput} nor ${contentInput} was set`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Build messages array from either prompt config or legacy format
|
||||||
|
*/
|
||||||
|
function buildMessages(promptConfig, systemPrompt, prompt) {
|
||||||
|
if (promptConfig?.messages && promptConfig.messages.length > 0) {
|
||||||
|
// Use new message format
|
||||||
|
return promptConfig.messages.map(msg => ({
|
||||||
|
role: msg.role,
|
||||||
|
content: msg.content,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Use legacy format
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: systemPrompt || 'You are a helpful assistant',
|
||||||
|
},
|
||||||
|
{ role: 'user', content: prompt || '' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Build response format object for API from prompt config
|
||||||
|
*/
|
||||||
|
function buildResponseFormat(promptConfig) {
|
||||||
|
if (promptConfig?.responseFormat === 'json_schema' && promptConfig.jsonSchema) {
|
||||||
|
try {
|
||||||
|
const schema = JSON.parse(promptConfig.jsonSchema);
|
||||||
|
return {
|
||||||
|
type: 'json_schema',
|
||||||
|
json_schema: schema,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new Error(`Invalid JSON schema: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse custom headers from YAML or JSON format
|
||||||
|
* @param input - String in YAML or JSON format containing headers
|
||||||
|
* @returns Record of header names to values, or empty object if invalid
|
||||||
|
*/
|
||||||
|
function parseCustomHeaders(input) {
|
||||||
|
if (!input || input.trim() === '') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const trimmedInput = input.trim();
|
||||||
|
try {
|
||||||
|
// Try JSON first (check if it starts with { or [)
|
||||||
|
if (trimmedInput.startsWith('{') || trimmedInput.startsWith('[')) {
|
||||||
|
const parsed = JSON.parse(trimmedInput);
|
||||||
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||||
|
coreExports.warning('Custom headers JSON must be an object, not null or an array');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return validateAndMaskHeaders(parsed);
|
||||||
|
}
|
||||||
|
// Try YAML
|
||||||
|
const parsed = load(trimmedInput);
|
||||||
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||||
|
coreExports.warning('Custom headers YAML must be an object');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return validateAndMaskHeaders(parsed);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
coreExports.warning(`Failed to parse custom headers: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Validate header names and mask sensitive values in logs
|
||||||
|
* @param headers - Raw headers object
|
||||||
|
* @returns Validated headers with string values
|
||||||
|
*/
|
||||||
|
function validateAndMaskHeaders(headers) {
|
||||||
|
const validHeaders = {};
|
||||||
|
const sensitivePatterns = ['key', 'token', 'secret', 'password', 'authorization'];
|
||||||
|
for (const [name, value] of Object.entries(headers)) {
|
||||||
|
// Validate header name (basic HTTP header name validation, RFC 7230: letters, digits, and hyphens)
|
||||||
|
if (!/^[A-Za-z0-9-]+$/.test(name)) {
|
||||||
|
coreExports.warning(`Skipping invalid header name: ${name} (only alphanumeric characters and hyphens allowed)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Convert value to string
|
||||||
|
const stringValue = String(value);
|
||||||
|
// Validate header value to prevent CRLF/header injection
|
||||||
|
if (stringValue.includes('\r') || stringValue.includes('\n')) {
|
||||||
|
coreExports.warning(`Skipping header "${name}" because its value contains newline characters, which are not allowed in HTTP header values.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
validHeaders[name] = stringValue;
|
||||||
|
// Mask sensitive headers in logs
|
||||||
|
const lowerName = name.toLowerCase();
|
||||||
|
const isSensitive = sensitivePatterns.some(pattern => lowerName.includes(pattern));
|
||||||
|
if (isSensitive) {
|
||||||
|
coreExports.info(`Custom header added: ${name}: ***MASKED***`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
coreExports.info(`Custom header added: ${name}: ${stringValue}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return validHeaders;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Build complete InferenceRequest from prompt config and inputs
|
||||||
|
*/
|
||||||
|
function buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, temperature, topP, maxTokens, endpoint, token, customHeaders) {
|
||||||
|
const messages = buildMessages(promptConfig, systemPrompt, prompt);
|
||||||
|
const responseFormat = buildResponseFormat(promptConfig);
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
modelName,
|
||||||
|
temperature,
|
||||||
|
topP,
|
||||||
|
maxTokens,
|
||||||
|
endpoint,
|
||||||
|
token,
|
||||||
|
responseFormat,
|
||||||
|
customHeaders,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse template variables from YAML input string
|
* Parse template variables from YAML input string
|
||||||
*/
|
*/
|
||||||
@@ -61478,8 +61548,11 @@ async function run() {
|
|||||||
const githubMcpToken = coreExports.getInput('github-mcp-token') || token;
|
const githubMcpToken = coreExports.getInput('github-mcp-token') || token;
|
||||||
const githubMcpToolsets = coreExports.getInput('github-mcp-toolsets');
|
const githubMcpToolsets = coreExports.getInput('github-mcp-toolsets');
|
||||||
const endpoint = coreExports.getInput('endpoint');
|
const endpoint = coreExports.getInput('endpoint');
|
||||||
|
// Parse custom headers
|
||||||
|
const customHeadersInput = coreExports.getInput('custom-headers');
|
||||||
|
const customHeaders = parseCustomHeaders(customHeadersInput);
|
||||||
// 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, promptConfig?.modelParameters?.temperature, promptConfig?.modelParameters?.topP, maxTokens, endpoint, token);
|
const inferenceRequest = buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, promptConfig?.modelParameters?.temperature, promptConfig?.modelParameters?.topP, maxTokens, endpoint, token, customHeaders);
|
||||||
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) {
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
|
import * as yaml from 'js-yaml'
|
||||||
import {PromptConfig} from './prompt.js'
|
import {PromptConfig} from './prompt.js'
|
||||||
import {InferenceRequest} from './inference.js'
|
import {InferenceRequest} from './inference.js'
|
||||||
|
|
||||||
@@ -74,6 +75,83 @@ export function buildResponseFormat(
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse custom headers from YAML or JSON format
|
||||||
|
* @param input - String in YAML or JSON format containing headers
|
||||||
|
* @returns Record of header names to values, or empty object if invalid
|
||||||
|
*/
|
||||||
|
export function parseCustomHeaders(input: string): Record<string, string> {
|
||||||
|
if (!input || input.trim() === '') {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedInput = input.trim()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try JSON first (check if it starts with { or [)
|
||||||
|
if (trimmedInput.startsWith('{') || trimmedInput.startsWith('[')) {
|
||||||
|
const parsed = JSON.parse(trimmedInput)
|
||||||
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||||
|
core.warning('Custom headers JSON must be an object, not null or an array')
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return validateAndMaskHeaders(parsed as Record<string, unknown>)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try YAML
|
||||||
|
const parsed = yaml.load(trimmedInput)
|
||||||
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||||
|
core.warning('Custom headers YAML must be an object')
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return validateAndMaskHeaders(parsed as Record<string, unknown>)
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(`Failed to parse custom headers: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate header names and mask sensitive values in logs
|
||||||
|
* @param headers - Raw headers object
|
||||||
|
* @returns Validated headers with string values
|
||||||
|
*/
|
||||||
|
function validateAndMaskHeaders(headers: Record<string, unknown>): Record<string, string> {
|
||||||
|
const validHeaders: Record<string, string> = {}
|
||||||
|
const sensitivePatterns = ['key', 'token', 'secret', 'password', 'authorization']
|
||||||
|
|
||||||
|
for (const [name, value] of Object.entries(headers)) {
|
||||||
|
// Validate header name (basic HTTP header name validation, RFC 7230: letters, digits, and hyphens)
|
||||||
|
if (!/^[A-Za-z0-9-]+$/.test(name)) {
|
||||||
|
core.warning(`Skipping invalid header name: ${name} (only alphanumeric characters and hyphens allowed)`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert value to string
|
||||||
|
const stringValue = String(value)
|
||||||
|
|
||||||
|
// Validate header value to prevent CRLF/header injection
|
||||||
|
if (stringValue.includes('\r') || stringValue.includes('\n')) {
|
||||||
|
core.warning(
|
||||||
|
`Skipping header "${name}" because its value contains newline characters, which are not allowed in HTTP header values.`,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
validHeaders[name] = stringValue
|
||||||
|
|
||||||
|
// Mask sensitive headers in logs
|
||||||
|
const lowerName = name.toLowerCase()
|
||||||
|
const isSensitive = sensitivePatterns.some(pattern => lowerName.includes(pattern))
|
||||||
|
if (isSensitive) {
|
||||||
|
core.info(`Custom header added: ${name}: ***MASKED***`)
|
||||||
|
} else {
|
||||||
|
core.info(`Custom header added: ${name}: ${stringValue}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validHeaders
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build complete InferenceRequest from prompt config and inputs
|
* Build complete InferenceRequest from prompt config and inputs
|
||||||
*/
|
*/
|
||||||
@@ -87,6 +165,7 @@ export function buildInferenceRequest(
|
|||||||
maxTokens: number,
|
maxTokens: number,
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
token: string,
|
token: string,
|
||||||
|
customHeaders?: Record<string, string>,
|
||||||
): InferenceRequest {
|
): InferenceRequest {
|
||||||
const messages = buildMessages(promptConfig, systemPrompt, prompt)
|
const messages = buildMessages(promptConfig, systemPrompt, prompt)
|
||||||
const responseFormat = buildResponseFormat(promptConfig)
|
const responseFormat = buildResponseFormat(promptConfig)
|
||||||
@@ -100,5 +179,6 @@ export function buildInferenceRequest(
|
|||||||
endpoint,
|
endpoint,
|
||||||
token,
|
token,
|
||||||
responseFormat,
|
responseFormat,
|
||||||
|
customHeaders,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface InferenceRequest {
|
|||||||
temperature?: number
|
temperature?: number
|
||||||
topP?: 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
|
||||||
|
customHeaders?: Record<string, string> // Custom HTTP headers to include in API requests
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InferenceResponse {
|
export interface InferenceResponse {
|
||||||
@@ -41,6 +42,7 @@ export async function simpleInference(request: InferenceRequest): Promise<string
|
|||||||
const client = new OpenAI({
|
const client = new OpenAI({
|
||||||
apiKey: request.token,
|
apiKey: request.token,
|
||||||
baseURL: request.endpoint,
|
baseURL: request.endpoint,
|
||||||
|
defaultHeaders: request.customHeaders || {},
|
||||||
})
|
})
|
||||||
|
|
||||||
const chatCompletionRequest: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
|
const chatCompletionRequest: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
|
||||||
@@ -75,6 +77,7 @@ export async function mcpInference(
|
|||||||
const client = new OpenAI({
|
const client = new OpenAI({
|
||||||
apiKey: request.token,
|
apiKey: request.token,
|
||||||
baseURL: request.endpoint,
|
baseURL: request.endpoint,
|
||||||
|
defaultHeaders: request.customHeaders || {},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start with the pre-processed messages
|
// Start with the pre-processed messages
|
||||||
|
|||||||
+6
-1
@@ -3,7 +3,7 @@ import * as fs from 'fs'
|
|||||||
import * as tmp from 'tmp'
|
import * as tmp from 'tmp'
|
||||||
import {connectToGitHubMCP} from './mcp.js'
|
import {connectToGitHubMCP} from './mcp.js'
|
||||||
import {simpleInference, mcpInference} from './inference.js'
|
import {simpleInference, mcpInference} from './inference.js'
|
||||||
import {loadContentFromFileOrInput, buildInferenceRequest} from './helpers.js'
|
import {loadContentFromFileOrInput, buildInferenceRequest, parseCustomHeaders} from './helpers.js'
|
||||||
import {
|
import {
|
||||||
loadPromptFile,
|
loadPromptFile,
|
||||||
parseTemplateVariables,
|
parseTemplateVariables,
|
||||||
@@ -65,6 +65,10 @@ export async function run(): Promise<void> {
|
|||||||
|
|
||||||
const endpoint = core.getInput('endpoint')
|
const endpoint = core.getInput('endpoint')
|
||||||
|
|
||||||
|
// Parse custom headers
|
||||||
|
const customHeadersInput = core.getInput('custom-headers')
|
||||||
|
const customHeaders = parseCustomHeaders(customHeadersInput)
|
||||||
|
|
||||||
// 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(
|
const inferenceRequest = buildInferenceRequest(
|
||||||
promptConfig,
|
promptConfig,
|
||||||
@@ -76,6 +80,7 @@ export async function run(): Promise<void> {
|
|||||||
maxTokens,
|
maxTokens,
|
||||||
endpoint,
|
endpoint,
|
||||||
token,
|
token,
|
||||||
|
customHeaders,
|
||||||
)
|
)
|
||||||
|
|
||||||
const enableMcp = core.getBooleanInput('enable-github-mcp') || false
|
const enableMcp = core.getBooleanInput('enable-github-mcp') || false
|
||||||
|
|||||||
Reference in New Issue
Block a user