Skip to content

Commit 213716d

Browse files
authored
Merge pull request #1 from 0xeb/feature/make-tool-and-byok-env-squashed
Add make_tool API and BYOK environment variable support
2 parents d36125a + d3bc23d commit 213716d

13 files changed

Lines changed: 1026 additions & 43 deletions

File tree

.github/workflows/ci.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build:
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
os: [ubuntu-latest, windows-latest, macos-latest]
15+
include:
16+
- os: ubuntu-latest
17+
cmake_generator: "Unix Makefiles"
18+
- os: windows-latest
19+
cmake_generator: "Visual Studio 17 2022"
20+
- os: macos-latest
21+
cmake_generator: "Unix Makefiles"
22+
23+
runs-on: ${{ matrix.os }}
24+
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- name: Configure CMake
29+
run: cmake -B build -G "${{ matrix.cmake_generator }}" -DCMAKE_BUILD_TYPE=Release -DCOPILOT_BUILD_TESTS=ON
30+
31+
- name: Build
32+
run: cmake --build build --config Release
33+
34+
- name: Run Unit Tests
35+
run: ctest --test-dir build -C Release -E "^E2ETest\." --output-on-failure
36+
env:
37+
COPILOT_SDK_CPP_SKIP_E2E: 1

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
tests/byok.env
2+
tests/logs/
13
build*/
24
cmake-build*/
35
out*/

README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,106 @@ auto session = client.resume_session(session_id, resume_config).get();
6666
6767
See `examples/tools.cpp` and `examples/resume_with_tools.cpp` for complete examples.
6868
69+
### Fluent Tool Builder
70+
71+
Use `make_tool` to create tools with automatic schema generation from lambda signatures:
72+
73+
```cpp
74+
#include <copilot/tool_builder.hpp>
75+
76+
// Single parameter - schema auto-generated
77+
auto echo_tool = copilot::make_tool(
78+
"echo", "Echo a message",
79+
[](std::string message) { return message; },
80+
{"message"} // Parameter names
81+
);
82+
83+
// Multiple parameters
84+
auto calc_tool = copilot::make_tool(
85+
"add", "Add two numbers",
86+
[](double a, double b) { return std::to_string(a + b); },
87+
{"first", "second"}
88+
);
89+
90+
// Optional parameters (not added to "required" in schema)
91+
auto greet_tool = copilot::make_tool(
92+
"greet", "Greet someone",
93+
[](std::string name, std::optional<std::string> title) {
94+
if (title)
95+
return "Hello, " + *title + " " + name + "!";
96+
return "Hello, " + name + "!";
97+
},
98+
{"name", "title"}
99+
);
100+
101+
// Use in session config
102+
copilot::SessionConfig config;
103+
config.tools = {echo_tool, calc_tool, greet_tool};
104+
```
105+
106+
## BYOK (Bring Your Own Key)
107+
108+
Use your own API key instead of GitHub Copilot authentication.
109+
110+
### Method 1: Explicit Configuration
111+
112+
```cpp
113+
copilot::ProviderConfig provider;
114+
provider.api_key = "sk-your-api-key";
115+
provider.base_url = "https://api.openai.com/v1";
116+
provider.type = "openai";
117+
118+
copilot::SessionConfig config;
119+
config.provider = provider;
120+
config.model = "gpt-4";
121+
auto session = client.create_session(config).get();
122+
```
123+
124+
### Method 2: Environment Variables
125+
126+
Set environment variables:
127+
128+
```bash
129+
export COPILOT_SDK_BYOK_API_KEY=sk-your-api-key
130+
export COPILOT_SDK_BYOK_BASE_URL=https://api.openai.com/v1 # Optional, defaults to OpenAI
131+
export COPILOT_SDK_BYOK_PROVIDER_TYPE=openai # Optional, defaults to "openai"
132+
export COPILOT_SDK_BYOK_MODEL=gpt-4 # Optional
133+
```
134+
135+
Then enable auto-loading in your code:
136+
137+
```cpp
138+
copilot::SessionConfig config;
139+
config.auto_byok_from_env = true; // Load from COPILOT_SDK_BYOK_* env vars
140+
auto session = client.create_session(config).get();
141+
```
142+
143+
**Precedence** (for each field):
144+
1. Explicit value in `SessionConfig` (highest priority)
145+
2. Environment variable (if `auto_byok_from_env = true`)
146+
3. Default Copilot behavior (lowest priority)
147+
148+
**Note:** `auto_byok_from_env` defaults to `false` for backwards compatibility. Existing code will not be affected by setting these environment variables.
149+
150+
### Checking Environment Configuration
151+
152+
```cpp
153+
// Check if BYOK env vars are configured
154+
if (copilot::ProviderConfig::is_env_configured()) {
155+
// COPILOT_SDK_BYOK_API_KEY is set
156+
}
157+
158+
// Load provider config from env (returns nullopt if not configured)
159+
if (auto provider = copilot::ProviderConfig::from_env()) {
160+
// Use *provider
161+
}
162+
163+
// Load model from env
164+
if (auto model = copilot::ProviderConfig::model_from_env()) {
165+
// Use *model
166+
}
167+
```
168+
69169
## Install / Package
70170

71171
```sh

include/copilot/tool_builder.hpp

Lines changed: 195 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,20 @@ struct schema_type<std::optional<T>>
141141
static json schema() { return schema_type<T>::schema(); }
142142
};
143143

144+
// Type trait to detect std::optional
145+
template<typename T>
146+
struct is_optional : std::false_type
147+
{
148+
};
149+
150+
template<typename T>
151+
struct is_optional<std::optional<T>> : std::true_type
152+
{
153+
};
154+
155+
template<typename T>
156+
inline constexpr bool is_optional_v = is_optional<T>::value;
157+
144158
/// Convert value to string for tool result
145159
template<typename T>
146160
std::string to_result_string(const T& value)
@@ -170,7 +184,16 @@ std::string to_result_string(const T& value)
170184
template<typename T>
171185
T extract_arg(const json& args, const std::string& name)
172186
{
173-
if constexpr (std::is_same_v<std::decay_t<T>, std::string>)
187+
if constexpr (is_optional_v<std::decay_t<T>>)
188+
{
189+
using value_type = typename std::decay_t<T>::value_type;
190+
if (!args.contains(name) || args.at(name).is_null())
191+
{
192+
return std::nullopt;
193+
}
194+
return extract_arg<value_type>(args, name);
195+
}
196+
else if constexpr (std::is_same_v<std::decay_t<T>, std::string>)
174197
{
175198
return args.at(name).get<std::string>();
176199
}
@@ -191,20 +214,6 @@ T extract_arg_or(const json& args, const std::string& name, const T& default_val
191214
return default_val;
192215
}
193216

194-
// Type trait to detect std::optional
195-
template<typename T>
196-
struct is_optional : std::false_type
197-
{
198-
};
199-
200-
template<typename T>
201-
struct is_optional<std::optional<T>> : std::true_type
202-
{
203-
};
204-
205-
template<typename T>
206-
inline constexpr bool is_optional_v = is_optional<T>::value;
207-
208217
} // namespace detail
209218

210219
// =============================================================================
@@ -528,4 +537,175 @@ inline ToolBuilder tool(std::string name, std::string description)
528537
return ToolBuilder(std::move(name), std::move(description));
529538
}
530539

540+
// =============================================================================
541+
// Function Traits for make_tool
542+
// =============================================================================
543+
544+
namespace detail
545+
{
546+
547+
/// Remove cv-ref qualifiers
548+
template <typename T>
549+
using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>;
550+
551+
/// Function traits primary template
552+
template <typename T>
553+
struct function_traits : function_traits<decltype(&T::operator())>
554+
{
555+
};
556+
557+
/// Specialization for function pointers
558+
template <typename R, typename... Args>
559+
struct function_traits<R (*)(Args...)>
560+
{
561+
using return_type = R;
562+
static constexpr size_t arity = sizeof...(Args);
563+
564+
template <size_t N>
565+
using arg_type = std::tuple_element_t<N, std::tuple<Args...>>;
566+
};
567+
568+
/// Specialization for member function pointers (const)
569+
template <typename C, typename R, typename... Args>
570+
struct function_traits<R (C::*)(Args...) const>
571+
{
572+
using return_type = R;
573+
static constexpr size_t arity = sizeof...(Args);
574+
575+
template <size_t N>
576+
using arg_type = std::tuple_element_t<N, std::tuple<Args...>>;
577+
};
578+
579+
/// Specialization for member function pointers (non-const)
580+
template <typename C, typename R, typename... Args>
581+
struct function_traits<R (C::*)(Args...)>
582+
{
583+
using return_type = R;
584+
static constexpr size_t arity = sizeof...(Args);
585+
586+
template <size_t N>
587+
using arg_type = std::tuple_element_t<N, std::tuple<Args...>>;
588+
};
589+
590+
/// Helper to invoke function with JSON args
591+
template <typename Func, size_t... Is>
592+
auto invoke_with_json_impl(Func&& func, const json& args,
593+
const std::vector<std::string>& names, std::index_sequence<Is...>)
594+
{
595+
using traits = function_traits<remove_cvref_t<Func>>;
596+
return func(
597+
extract_arg<remove_cvref_t<typename traits::template arg_type<Is>>>(args, names[Is])...);
598+
}
599+
600+
template <typename Func>
601+
auto invoke_with_json(Func&& func, const json& args, const std::vector<std::string>& names)
602+
{
603+
using traits = function_traits<remove_cvref_t<Func>>;
604+
return invoke_with_json_impl<Func>(std::forward<Func>(func), args, names,
605+
std::make_index_sequence<traits::arity>{});
606+
}
607+
608+
template<typename T>
609+
void add_required_if(json& required, const std::string& name)
610+
{
611+
if constexpr (!is_optional_v<T>)
612+
{
613+
required.push_back(name);
614+
}
615+
}
616+
617+
/// Generate schema from function signature
618+
template <typename Func, size_t... Is>
619+
json generate_schema_impl(const std::vector<std::string>& names, std::index_sequence<Is...>)
620+
{
621+
using traits = function_traits<remove_cvref_t<Func>>;
622+
json schema = {{"type", "object"}, {"properties", json::object()}, {"required", json::array()}};
623+
624+
((schema["properties"][names[Is]] =
625+
schema_type<remove_cvref_t<typename traits::template arg_type<Is>>>::schema(),
626+
add_required_if<remove_cvref_t<typename traits::template arg_type<Is>>>(
627+
schema["required"], names[Is])),
628+
...);
629+
630+
return schema;
631+
}
632+
633+
template <typename Func>
634+
json generate_schema(const std::vector<std::string>& names)
635+
{
636+
using traits = function_traits<remove_cvref_t<Func>>;
637+
return generate_schema_impl<Func>(names, std::make_index_sequence<traits::arity>{});
638+
}
639+
640+
} // namespace detail
641+
642+
// =============================================================================
643+
// make_tool - Claude SDK compatible API
644+
// =============================================================================
645+
646+
/// Create a tool from a function with custom parameter names
647+
/// Similar to claude::mcp::make_tool for API consistency
648+
///
649+
/// Example:
650+
/// @code
651+
/// auto tool = copilot::make_tool("dbg_exec", "Execute debugger command",
652+
/// [](std::string command) { return execute(command); },
653+
/// {"command"});
654+
/// @endcode
655+
template <typename Func>
656+
Tool make_tool(std::string name, std::string description, Func&& func,
657+
std::vector<std::string> param_names)
658+
{
659+
using traits = detail::function_traits<detail::remove_cvref_t<Func>>;
660+
661+
if (param_names.size() != traits::arity)
662+
{
663+
throw std::invalid_argument("Parameter name count mismatch for tool '" + name +
664+
"': expected " + std::to_string(traits::arity) + ", got " +
665+
std::to_string(param_names.size()));
666+
}
667+
668+
Tool tool;
669+
tool.name = std::move(name);
670+
tool.description = std::move(description);
671+
tool.parameters_schema = detail::generate_schema<Func>(param_names);
672+
673+
// Create handler that extracts args and invokes function
674+
tool.handler = [f = std::forward<Func>(func),
675+
names = std::move(param_names)](const ToolInvocation& inv) -> ToolResultObject
676+
{
677+
ToolResultObject result;
678+
try
679+
{
680+
json args = inv.arguments.value_or(json::object());
681+
auto output = detail::invoke_with_json(f, args, names);
682+
result.text_result_for_llm = detail::to_result_string(output);
683+
}
684+
catch (const std::exception& e)
685+
{
686+
result.result_type = "error";
687+
result.error = e.what();
688+
}
689+
return result;
690+
};
691+
692+
return tool;
693+
}
694+
695+
/// Create a tool with a single string parameter (common case)
696+
/// @code
697+
/// auto tool = copilot::make_tool("echo", "Echo message",
698+
/// [](std::string msg) { return msg; }); // Auto-names param "arg0"
699+
/// @endcode
700+
template <typename Func>
701+
Tool make_tool(std::string name, std::string description, Func&& func)
702+
{
703+
using traits = detail::function_traits<detail::remove_cvref_t<Func>>;
704+
std::vector<std::string> names;
705+
for (size_t i = 0; i < traits::arity; ++i)
706+
names.push_back("arg" + std::to_string(i));
707+
return make_tool(std::move(name), std::move(description), std::forward<Func>(func),
708+
std::move(names));
709+
}
710+
531711
} // namespace copilot

0 commit comments

Comments
 (0)